PK!CZ))orator/__init__.py# -*- coding: utf-8 -*- __version__ = "0.9.8" from .orm import Model, SoftDeletes, Collection, accessor, mutator, scope from .database_manager import DatabaseManager from .query.expression import QueryExpression from .schema import Schema from .pagination import Paginator, LengthAwarePaginator PK!orator/commands/__init__.py# -*- coding: utf-8 -*- PK!7'orator/commands/application.py# -*- coding: utf-8 -*- from cleo import Application from .. import __version__ application = Application('Orator', __version__, complete=True) # Migrations from .migrations import ( InstallCommand, MigrateCommand, MigrateMakeCommand, RollbackCommand, StatusCommand, ResetCommand, RefreshCommand ) application.add(InstallCommand()) application.add(MigrateCommand()) application.add(MigrateMakeCommand()) application.add(RollbackCommand()) application.add(StatusCommand()) application.add(ResetCommand()) application.add(RefreshCommand()) # Seeds from .seeds import SeedersMakeCommand, SeedCommand application.add(SeedersMakeCommand()) application.add(SeedCommand()) # Models from .models import ModelMakeCommand application.add(ModelMakeCommand()) PK!?u.p p orator/commands/command.py# -*- coding: utf-8 -*- import os from cleo import Command as BaseCommand, InputOption, ListInput from orator import DatabaseManager import yaml class Command(BaseCommand): needs_config = True def __init__(self, resolver=None): self.resolver = resolver self.input = None self.output = None super(Command, self).__init__() def configure(self): super(Command, self).configure() if self.needs_config and not self.resolver: # Checking if a default config file is present if not self._check_config(): self.add_option('config', 'c', InputOption.VALUE_REQUIRED, 'The config file path') def execute(self, i, o): """ Executes the command. """ self.set_style('question', fg='blue') if self.needs_config and not self.resolver: self._handle_config(self.option('config')) return self.handle() def call(self, name, options=None): command = self.get_application().find(name) command.resolver = self.resolver return super(Command, self).call(name, options) def call_silent(self, name, options=None): command = self.get_application().find(name) command.resolver = self.resolver return super(Command, self).call_silent(name, options) def confirm_to_proceed(self, message=None): if message is None: message = 'Do you really wish to run this command?: ' if self.option('force'): return True confirmed = self.confirm(message) if not confirmed: self.comment('Command Cancelled!') return False return True def _get_migration_path(self): return os.path.join(os.getcwd(), 'migrations') def _check_config(self): """ Check presence of default config files. :rtype: bool """ current_path = os.path.relpath(os.getcwd()) accepted_files = ['orator.yml', 'orator.py'] for accepted_file in accepted_files: config_file = os.path.join(current_path, accepted_file) if os.path.exists(config_file): if self._handle_config(config_file): return True return False def _handle_config(self, config_file): """ Check and handle a config file. :param config_file: The path to the config file :type config_file: str :rtype: bool """ config = self._get_config(config_file) self.resolver = DatabaseManager(config.get('databases', config.get('DATABASES', {}))) return True def _get_config(self, path=None): """ Get the config. :rtype: dict """ if not path and not self.option('config'): raise Exception('The --config|-c option is missing.') if not path: path = self.option('config') filename, ext = os.path.splitext(path) if ext in ['.yml', '.yaml']: with open(path) as fd: config = yaml.load(fd) elif ext in ['.py']: config = {} with open(path) as fh: exec(fh.read(), {}, config) else: raise RuntimeError('Config file [%s] is not supported.' % path) return config PK!U~JJ&orator/commands/migrations/__init__.py# -*- coding: utf-8 -*- from .install_command import InstallCommand from .migrate_command import MigrateCommand from .make_command import MigrateMakeCommand from .rollback_command import RollbackCommand from .status_command import StatusCommand from .reset_command import ResetCommand from .refresh_command import RefreshCommand PK!}?*orator/commands/migrations/base_command.py# -*- coding: utf-8 -*- import os from ..command import Command class BaseCommand(Command): def _get_migration_path(self): return os.path.join(os.getcwd(), 'migrations') PK!4$rr-orator/commands/migrations/install_command.py# -*- coding: utf-8 -*- from orator.migrations import DatabaseMigrationRepository from .base_command import BaseCommand class InstallCommand(BaseCommand): """ Create the migration repository. migrate:install {--d|database= : The database connection to use.} """ def handle(self): """ Executes the command """ database = self.option('database') repository = DatabaseMigrationRepository(self.resolver, 'migrations') repository.set_source(database) repository.create_repository() self.info('Migration table created successfully') PK!nAA*orator/commands/migrations/make_command.py# -*- coding: utf-8 -*- import os from orator.migrations import MigrationCreator from .base_command import BaseCommand class MigrateMakeCommand(BaseCommand): """ Create a new migration file. make:migration {name : The name of the migration.} {--t|table= : The table to create the migration for.} {--C|create : Whether the migration will create the table or not.} {--p|path= : The path to migrations files.} """ needs_config = False def handle(self): """ Executes the command. """ creator = MigrationCreator() name = self.argument('name') table = self.option('table') create = bool(self.option('create')) if not table and create is not False: table = create path = self.option('path') if path is None: path = self._get_migration_path() migration_name = self._write_migration( creator, name, table, create, path ) self.line( 'Created migration: {}'.format(migration_name) ) def _write_migration(self, creator, name, table, create, path): """ Write the migration file to disk. """ file_ = os.path.basename(creator.create(name, path, table, create)) return file_ PK!FE E -orator/commands/migrations/migrate_command.py# -*- coding: utf-8 -*- from orator.migrations import Migrator, DatabaseMigrationRepository from .base_command import BaseCommand class MigrateCommand(BaseCommand): """ Run the database migrations. migrate {--d|database= : The database connection to use.} {--p|path= : The path of migrations files to be executed.} {--s|seed : Indicates if the seed task should be re-run.} {--seed-path= : The path of seeds files to be executed. Defaults to ./seeders.} {--P|pretend : Dump the SQL queries that would be run.} {--f|force : Force the operation to run.} """ def handle(self): if not self.confirm_to_proceed( 'Are you sure you want to proceed with the migration? ' ): return database = self.option('database') repository = DatabaseMigrationRepository(self.resolver, 'migrations') migrator = Migrator(repository, self.resolver) self._prepare_database(migrator, database) pretend = self.option('pretend') path = self.option('path') if path is None: path = self._get_migration_path() migrator.run(path, pretend) for note in migrator.get_notes(): self.line(note) # If the "seed" option has been given, we will rerun the database seed task # to repopulate the database. if self.option('seed'): options = [ ('--force', self.option('force')) ] if database: options.append(('--database', database)) if self.get_definition().has_option('config'): options.append(('--config', self.option('config'))) if self.option('seed-path'): options.append(('--path', self.option('seed-path'))) self.call('db:seed', options) def _prepare_database(self, migrator, database): migrator.set_connection(database) if not migrator.repository_exists(): options = [] if database: options.append(('--database', database)) if self.get_definition().has_option('config'): options.append(('--config', self.option('config'))) self.call('migrate:install', options) PK!V-orator/commands/migrations/refresh_command.py# -*- coding: utf-8 -*- from .base_command import BaseCommand class RefreshCommand(BaseCommand): """ Reset and re-run all migrations. migrate:refresh {--d|database= : The database connection to use.} {--p|path= : The path of migrations files to be executed.} {--s|seed : Indicates if the seed task should be re-run.} {--seed-path= : The path of seeds files to be executed. Defaults to ./seeds.} {--seeder=database_seeder : The name of the root seeder.} {--f|force : Force the operation to run.} """ def handle(self): """ Executes the command. """ if not self.confirm_to_proceed( 'Are you sure you want to refresh the database?: ' ): return database = self.option('database') options = [ ('--force', True) ] if self.option('path'): options.append(('--path', self.option('path'))) if database: options.append(('--database', database)) if self.get_definition().has_option('config'): options.append(('--config', self.option('config'))) self.call('migrate:reset', options) self.call('migrate', options) if self._needs_seeding(): self._run_seeder(database) def _needs_seeding(self): return self.option('seed') def _run_seeder(self, database): options = [ ('--seeder', self.option('seeder')), ('--force', True) ] if database: options.append(('--database', database)) if self.get_definition().has_option('config'): options.append(('--config', self.option('config'))) if self.option('seed-path'): options.append(('--path', self.option('seed-path'))) self.call('db:seed', options) PK! G3+orator/commands/migrations/reset_command.py# -*- coding: utf-8 -*- from orator.migrations import Migrator, DatabaseMigrationRepository from .base_command import BaseCommand class ResetCommand(BaseCommand): """ Rollback all database migrations. migrate:reset {--d|database= : The database connection to use.} {--p|path= : The path of migrations files to be executed.} {--P|pretend : Dump the SQL queries that would be run.} {--f|force : Force the operation to run.} """ def handle(self): """ Executes the command. """ if not self.confirm_to_proceed( 'Are you sure you want to reset all of the migrations?: ' ): return database = self.option('database') repository = DatabaseMigrationRepository(self.resolver, 'migrations') migrator = Migrator(repository, self.resolver) self._prepare_database(migrator, database) pretend = bool(self.option('pretend')) path = self.option('path') if path is None: path = self._get_migration_path() migrator.reset(path, pretend) for note in migrator.get_notes(): self.line(note) def _prepare_database(self, migrator, database): migrator.set_connection(database) PK!^`##.orator/commands/migrations/rollback_command.py# -*- coding: utf-8 -*- from orator.migrations import Migrator, DatabaseMigrationRepository from .base_command import BaseCommand class RollbackCommand(BaseCommand): """ Rollback the last database migration. migrate:rollback {--d|database= : The database connection to use.} {--p|path= : The path of migrations files to be executed.} {--P|pretend : Dump the SQL queries that would be run.} {--f|force : Force the operation to run.} """ def handle(self): """ Executes the command. """ if not self.confirm_to_proceed( 'Are you sure you want to rollback the last migration?: ' ): return database = self.option('database') repository = DatabaseMigrationRepository(self.resolver, 'migrations') migrator = Migrator(repository, self.resolver) self._prepare_database(migrator, database) pretend = self.option('pretend') path = self.option('path') if path is None: path = self._get_migration_path() migrator.rollback(path, pretend) for note in migrator.get_notes(): self.line(note) def _prepare_database(self, migrator, database): migrator.set_connection(database) PK!,orator/commands/migrations/status_command.py# -*- coding: utf-8 -*- from orator.migrations import Migrator, DatabaseMigrationRepository from .base_command import BaseCommand class StatusCommand(BaseCommand): """ Show a list of migrations up/down. migrate:status {--d|database= : The database connection to use.} {--p|path= : The path of migrations files to be executed.} """ def handle(self): """ Executes the command. """ database = self.option('database') self.resolver.set_default_connection(database) repository = DatabaseMigrationRepository(self.resolver, 'migrations') migrator = Migrator(repository, self.resolver) if not migrator.repository_exists(): return self.error('No migrations found') self._prepare_database(migrator, database) path = self.option('path') if path is None: path = self._get_migration_path() ran = migrator.get_repository().get_ran() migrations = [] for migration in migrator._get_migration_files(path): if migration in ran: migrations.append(['%s' % migration, 'Yes']) else: migrations.append(['%s' % migration, 'No']) if migrations: table = self.table( ['Migration', 'Ran?'], migrations ) table.render() else: return self.error('No migrations found') for note in migrator.get_notes(): self.line(note) def _prepare_database(self, migrator, database): migrator.set_connection(database) PK!9׬%DD"orator/commands/models/__init__.py# -*- coding: utf-8 -*- from .make_command import ModelMakeCommand PK!ق&orator/commands/models/make_command.py# -*- coding: utf-8 -*- import os import inflection from cleo import Command from .stubs import MODEL_DEFAULT_STUB from ...utils import mkdir_p class ModelMakeCommand(Command): """ Creates a new Model class. make:model {name : The name of the model to create.} {--m|migration : Create a new migration file for the model.} {--p|path= : Path to models directory} """ def handle(self): name = self.argument('name') singular = inflection.singularize(inflection.tableize(name)) directory = self._get_path() filepath = self._get_path(singular + '.py') if os.path.exists(filepath): raise RuntimeError('The model file already exists.') mkdir_p(directory) parent = os.path.join(directory, '__init__.py') if not os.path.exists(parent): with open(parent, 'w'): pass stub = self._get_stub() stub = self._populate_stub(name, stub) with open(filepath, 'w') as f: f.write(stub) self.info('Model %s successfully created.' % name) if self.option('migration'): table = inflection.tableize(name) self.call( 'make:migration', [ ('name', 'create_%s_table' % table), ('--table', table), ('--create', True) ] ) def _get_stub(self): """ Get the model stub template :rtype: str """ return MODEL_DEFAULT_STUB def _populate_stub(self, name, stub): """ Populate the placeholders in the migration stub. :param name: The name of the model :type name: str :param stub: The stub :type stub: str :rtype: str """ stub = stub.replace('DummyClass', name) return stub def _get_path(self, name=None): if self.option('path'): directory = self.option('path') else: directory = os.path.join(os.getcwd(), 'models') if name: return os.path.join(directory, name) return directory PK!issorator/commands/models/stubs.py# -*- coding: utf-8 -*- MODEL_DEFAULT_STUB = """from orator import Model class DummyClass(Model): pass """ PK!i#Wll!orator/commands/seeds/__init__.py# -*- coding: utf-8 -*- from .make_command import SeedersMakeCommand from .seed_command import SeedCommand PK!QE2%orator/commands/seeds/base_command.py# -*- coding: utf-8 -*- import os from ..command import Command class BaseCommand(Command): def _get_seeders_path(self): return os.path.join(os.getcwd(), 'seeds') PK!B%orator/commands/seeds/make_command.py# -*- coding: utf-8 -*- import os import errno import inflection from ...seeds.stubs import DEFAULT_STUB from .base_command import BaseCommand class SeedersMakeCommand(BaseCommand): """ Create a new seeder file. make:seed {name : The name of the seed.} {--p|path= : The path to seeders files. Defaults to ./seeds.} """ needs_config = False def handle(self): """ Executes the command. """ # Making root seeder self._make('database_seeder', True) self._make(self.argument('name')) def _make(self, name, root=False): name = self._parse_name(name) path = self._get_path(name) if os.path.exists(path): if not root: self.error('%s already exists' % name) return False self._make_directory(os.path.dirname(path)) with open(path, 'w') as fh: fh.write(self._build_class(name)) if root: with open(os.path.join(os.path.dirname(path), '__init__.py'), 'w'): pass self.info('%s created successfully.' % name) def _parse_name(self, name): if name.endswith('.py'): name = name.replace('.py', '', -1) return name def _get_path(self, name): """ Get the destination class path. :param name: The name :type name: str :rtype: str """ path = self.option('path') if path is None: path = self._get_seeders_path() return os.path.join(path, '%s.py' % name) def _make_directory(self, path): try: os.makedirs(path) except OSError as exc: if exc.errno == errno.EEXIST and os.path.isdir(path): pass else: raise def _build_class(self, name): stub = self._get_stub() klass = self._get_class_name(name) stub = stub.replace('DummyClass', klass) return stub def _get_stub(self): return DEFAULT_STUB def _get_class_name(self, name): return inflection.camelize(name) PK!hfWe%orator/commands/seeds/seed_command.py# -*- coding: utf-8 -*- import importlib import inflection import os from cleo import InputOption from orator import DatabaseManager from .base_command import BaseCommand from ...utils import load_module class SeedCommand(BaseCommand): """ Seed the database with records. db:seed {--d|database= : The database connection to use.} {--p|path= : The path to seeders files. Defaults to ./seeds.} {--seeder=database_seeder : The name of the root seeder.} {--f|force : Force the operation to run.} """ def handle(self): """ Executes the command. """ if not self.confirm_to_proceed( 'Are you sure you want to seed the database?: ' ): return self.resolver.set_default_connection(self.option('database')) self._get_seeder().run() self.info('Database seeded!') def _get_seeder(self): name = self._parse_name(self.option('seeder')) seeder_file = self._get_path(name) # Loading parent module load_module('seeds', self._get_path('__init__')) # Loading module mod = load_module('seeds.%s' % name, seeder_file) klass = getattr(mod, inflection.camelize(name)) instance = klass() instance.set_command(self) instance.set_connection_resolver(self.resolver) return instance def _parse_name(self, name): if name.endswith('.py'): name = name.replace('.py', '', -1) return name def _get_path(self, name): """ Get the destination class path. :param name: The name :type name: str :rtype: str """ path = self.option('path') if path is None: path = self._get_seeders_path() return os.path.join(path, '%s.py' % name) PK! orator/connections/__init__.py# -*- coding: utf-8 -*- from .connection import Connection from .mysql_connection import MySQLConnection from .postgres_connection import PostgresConnection from .sqlite_connection import SQLiteConnection PK!y:: orator/connections/connection.py# -*- coding: utf-8 -*- import time import logging from functools import wraps from contextlib import contextmanager from .connection_interface import ConnectionInterface from ..query.grammars.grammar import QueryGrammar from ..query import QueryBuilder from ..query.expression import QueryExpression from ..query.processors.processor import QueryProcessor from ..schema.builder import SchemaBuilder from ..dbal.schema_manager import SchemaManager from ..exceptions.query import QueryException query_logger = logging.getLogger('orator.connection.queries') connection_logger = logging.getLogger('orator.connection') def run(wrapped): """ Special decorator encapsulating query method. """ @wraps(wrapped) def _run(self, query, bindings=None, *args, **kwargs): self._reconnect_if_missing_connection() start = time.time() try: result = wrapped(self, query, bindings, *args, **kwargs) except Exception as e: result = self._try_again_if_caused_by_lost_connection( e, query, bindings, wrapped ) t = self._get_elapsed_time(start) self.log_query(query, bindings, t) return result return _run class Connection(ConnectionInterface): name = None def __init__(self, connection, database='', table_prefix='', config=None, builder_class=QueryBuilder, builder_default_kwargs=None): """ :param connection: A dbapi connection instance :type connection: Connector :param database: The database name :type database: str :param table_prefix: The table prefix :type table_prefix: str :param config: The connection configuration :type config: dict """ self._connection = connection self._cursor = None self._read_connection = None self._database = database if table_prefix is None: table_prefix = '' self._table_prefix = table_prefix if config is None: config = {} self._config = config self._reconnector = None self._transactions = 0 self._pretending = False self._builder_class = builder_class if builder_default_kwargs is None: builder_default_kwargs = {} self._builder_default_kwargs = builder_default_kwargs self._logging_queries = config.get('log_queries', False) self._logged_queries = [] # Setting the marker based on config self._marker = None if self._config.get('use_qmark'): self._marker = '?' self._query_grammar = self.get_default_query_grammar() self._schema_grammar = None self._post_processor = self.get_default_post_processor() self._server_version = None self.use_default_query_grammar() def use_default_query_grammar(self): self._query_grammar = self.get_default_query_grammar() def get_default_query_grammar(self): return QueryGrammar() def use_default_schema_grammar(self): self._schema_grammar = self.get_default_schema_grammar() def get_default_schema_grammar(self): pass def use_default_post_processor(self): self._post_processor = self.get_default_post_processor() def get_default_post_processor(self): return QueryProcessor() def get_database_platform(self): return self._connection.get_database_platform() def get_schema_builder(self): """ Retturn the underlying schema builder. :rtype: orator.schema.builder.SchemaBuilder """ if not self._schema_grammar: self.use_default_schema_grammar() return SchemaBuilder(self) def table(self, table): """ Begin a fluent query against a database table :param table: The database table :type table: str :return: A QueryBuilder instance :rtype: QueryBuilder """ query = self.query() return query.from_(table) def query(self): """ Begin a fluent query :return: A QueryBuilder instance :rtype: QueryBuilder """ query = self._builder_class( self, self._query_grammar, self._post_processor, **self._builder_default_kwargs ) return query def raw(self, value): return QueryExpression(value) def select_one(self, query, bindings=None): if bindings is None: bindings = {} records = self.select(query, bindings) if len(records): return records[1] return None def select_from_write_connection(self, query, bindings=None): if bindings is None: bindings = {} return self.select(query, bindings) @run def select(self, query, bindings=None, use_read_connection=True): if self.pretending(): return [] bindings = self.prepare_bindings(bindings) cursor = self._get_cursor_for_select(use_read_connection) cursor.execute(query, bindings) return cursor.fetchall() def select_many(self, size, query, bindings=None, use_read_connection=True, abort=False): if self.pretending(): yield [] else: bindings = self.prepare_bindings(bindings) cursor = self._get_cursor_for_select(use_read_connection) try: cursor.execute(query, bindings) except Exception as e: if self._caused_by_lost_connection(e) and not abort: self.reconnect() for results in self.select_many(size, query, bindings, use_read_connection, True): yield results else: raise else: results = cursor.fetchmany(size) while results: yield results results = cursor.fetchmany(size) def _get_cursor_for_select(self, use_read_connection=True): if use_read_connection: self._cursor = self.get_read_connection().cursor() else: self._cursor = self.get_connection().cursor() return self._cursor def insert(self, query, bindings=None): return self.statement(query, bindings) def update(self, query, bindings=None): return self.affecting_statement(query, bindings) def delete(self, query, bindings=None): return self.affecting_statement(query, bindings) @run def statement(self, query, bindings=None): if self.pretending(): return True bindings = self.prepare_bindings(bindings) return self._new_cursor().execute(query, bindings) @run def affecting_statement(self, query, bindings=None): if self.pretending(): return True bindings = self.prepare_bindings(bindings) cursor = self._new_cursor() cursor.execute(query, bindings) return cursor.rowcount def _new_cursor(self): self._cursor = self.get_connection().cursor() return self._cursor def get_cursor(self): return self._cursor @run def unprepared(self, query): if self.pretending(): return True return bool(self.get_connection().execute(query)) def prepare_bindings(self, bindings): if bindings is None: return [] return bindings @contextmanager def transaction(self): self.begin_transaction() try: yield self except Exception as e: self.rollback() raise try: self.commit() except Exception: self.rollback() raise def begin_transaction(self): self._transactions += 1 def commit(self): if self._transactions == 1: self._connection.commit() self._transactions -= 1 def rollback(self): if self._transactions == 1: self._transactions = 0 self._connection.rollback() else: self._transactions -= 1 def transaction_level(self): return self._transactions @contextmanager def pretend(self): self._logged_queries = [] self._pretending = True try: yield self except Exception: self._pretending = False self._pretending = False def _try_again_if_caused_by_lost_connection(self, e, query, bindings, callback, *args, **kwargs): if self._caused_by_lost_connection(e): self.reconnect() return callback(self, query, bindings, *args, **kwargs) raise QueryException(query, bindings, e) def _caused_by_lost_connection(self, e): message = str(e).lower() for s in ['server has gone away', 'no connection to the server', 'lost connection', 'is dead or not enabled', 'error while sending', 'decryption failed or bad record mac', 'server closed the connection unexpectedly', 'ssl connection has been closed unexpectedly', 'error writing data to the connection', 'resource deadlock avoided',]: if s in message: return True return False def disconnect(self): connection_logger.debug('%s is disconnecting' % self.__class__.__name__) if self._connection: self._connection.close() if self._read_connection and self._connection != self._read_connection: self._read_connection.close() self.set_connection(None).set_read_connection(None) connection_logger.debug('%s disconnected' % self.__class__.__name__) def reconnect(self): connection_logger.debug('%s is reconnecting' % self.__class__.__name__) if self._reconnector is not None and callable(self._reconnector): return self._reconnector(self) raise Exception('Lost connection and no reconnector available') def _reconnect_if_missing_connection(self): if self.get_connection() is None or self.get_read_connection() is None: self.reconnect() def log_query(self, query, bindings, time_=None): if self.pretending(): self._logged_queries.append(self._get_cursor_query(query, bindings)) if not self._logging_queries: return query = self._get_cursor_query(query, bindings) if query: log = 'Executed %s' % (query,) if time_: log += ' in %sms' % time_ query_logger.debug(log, extra={ 'query': query, 'bindings': bindings, 'elapsed_time': time_ }) def _get_elapsed_time(self, start): return round((time.time() - start) * 1000, 2) def _get_cursor_query(self, query, bindings): if self._pretending: return query, bindings return query, bindings def get_logged_queries(self): return self._logged_queries def get_connection(self): return self._connection def get_read_connection(self): if self._transactions >= 1: return self.get_connection() if self._read_connection is not None: return self._read_connection return self._connection def set_connection(self, connection): if self._transactions >= 1: raise RuntimeError("Can't swap dbapi connection" "while within transaction.") self._connection = connection return self def set_read_connection(self, connection): self._read_connection = connection return self def set_reconnector(self, reconnector): self._reconnector = reconnector return self def get_name(self): return self._config.get('name') def get_config(self, option): return self._config.get(option) def get_query_grammar(self): return self._query_grammar def set_query_grammar(self, grammar): self._query_grammar = grammar def get_schema_grammar(self): return self._schema_grammar def set_schema_grammar(self, grammar): self._schema_grammar = grammar def get_post_processor(self): """ Get the query post processor used by the connection :return: The query post processor :rtype: QueryProcessor """ return self._post_processor def set_post_processor(self, processor): """ Set the query post processor used by the connection :param processor: The query post processor :type processor: QueryProcessor """ self._post_processor = processor def pretending(self): return self._pretending def enable_query_log(self): self._logging_queries = True def disable_query_log(self): self._logging_queries = False def logging(self): return self._logging_queries def get_database_name(self): return self._database def get_table_prefix(self): return self._table_prefix def set_table_prefix(self, prefix): self._table_prefix = prefix self.get_query_grammar().set_table_prefix(prefix) def with_table_prefix(self, grammar): grammar.set_table_prefix(self._table_prefix) return grammar def get_column(self, table, column): schema = self.get_schema_manager() return schema.list_table_details(table).get_column(column) def get_schema_manager(self): return SchemaManager(self) def get_params(self): return self._connection.get_params() def get_marker(self): return self._marker def set_builder_class(self, klass, default_kwargs=None): self._builder_class = klass if default_kwargs is not None: self._builder_default_kwargs = default_kwargs return self def __enter__(self): self.begin_transaction() return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: try: self.commit() except Exception: self.rollback() raise else: self.rollback() raise (exc_type, exc_val, exc_tb) @property def server_version(self): if self._server_version is None: self._server_version = self.get_server_version() return self._server_version def get_server_version(self): return self._connection.get_server_version() PK!˸*orator/connections/connection_interface.py# -*- coding: utf-8 -*- class ConnectionInterface(object): def table(self, table): """ Begin a fluent query against a database table :param table: The database table :type table: str :return: A QueryBuilder instance :rtype: QueryBuilder """ raise NotImplementedError() def query(self): """ Begin a fluent query :return: A QueryBuilder instance :rtype: QueryBuilder """ raise NotImplementedError() def raw(self, value): """ Get a new raw query expression :param value: The value :type value: mixed :return: A QueryExpression instance :rtype: QueryExpression """ raise NotImplementedError() def select_one(self, query, bindings=None): """ Run a select statement and return a single result :param query: The select statement :type query: str :param bindings: The query bindings :type bindings: dict :return: mixed """ raise NotImplementedError() def select(self, query, bindings=None): """ Run a select statement against the database :param query: The select statement :type query: str :param bindings: The query bindings :type bindings: dict :return: mixed """ raise NotImplementedError() def insert(self, query, bindings=None): """ Run an insert statement against the database :param query: The insert statement :type query: str :param bindings: The query bindings :type bindings: dict :return: mixed """ raise NotImplementedError() def update(self, query, bindings=None): """ Run an update statement against the database :param query: The update statement :type query: str :param bindings: The query bindings :type bindings: dict :return: mixed """ raise NotImplementedError() def delete(self, query, bindings=None): """ Run a delete statement against the database :param query: The select statement :type query: str :param bindings: The query bindings :type bindings: dict :return: mixed """ raise NotImplementedError() def statement(self, query, bindings=None): """ Run an SQL statement and return the boolean result :param query: The select statement :type query: str :param bindings: The query bindings :type bindings: dict :return: Boolean result :rtype: bool """ raise NotImplementedError() def affecting_statement(self, query, bindings=None): """ Run an SQL statement and return the number of affected rows :param query: The select statement :type query: str :param bindings: The query bindings :type bindings: dict :return: Number of affected rows :rtype: int """ raise NotImplementedError() def unprepared(self, query): """ Run a raw, unprepared query against the dbapi connection :param query: The raw query :type query: str :return: Boolean result :rtype: bool """ raise NotImplementedError() def prepare_bindings(self, bindings): """ Prepare the query bindings for execution :param bindings: The query bindings :type bindings: dict :return: The prepared bindings :rtype: dict """ raise NotImplementedError() def transaction(self): raise NotImplementedError() def begin_transaction(self): raise NotImplementedError() def commit(self): raise NotImplementedError() def rollback(self): raise NotImplementedError() def transaction_level(self): raise NotImplementedError() def pretend(self): raise NotImplementedError() PK!#-++3orator/connections/connection_resolver_interface.py# -*- coding: utf-8 -*- class ConnectionResolverInterface(object): def connection(self, name=None): raise NotImplementedError() def get_default_connection(self): raise NotImplementedError() def set_default_connection(self, name): raise NotImplementedError() PK!$&orator/connections/mysql_connection.py# -*- coding: utf-8 -*- from ..utils import decode from ..utils import PY2 from .connection import Connection from ..query.grammars.mysql_grammar import MySQLQueryGrammar from ..query.processors.mysql_processor import MySQLQueryProcessor from ..schema.grammars import MySQLSchemaGrammar from ..schema import MySQLSchemaBuilder from ..dbal.mysql_schema_manager import MySQLSchemaManager class MySQLConnection(Connection): name = 'mysql' def get_default_query_grammar(self): return MySQLQueryGrammar(marker=self._marker) def get_default_post_processor(self): return MySQLQueryProcessor() def get_schema_builder(self): """ Return the underlying schema builder. :rtype: orator.schema.SchemaBuilder """ if not self._schema_grammar: self.use_default_schema_grammar() return MySQLSchemaBuilder(self) def get_default_schema_grammar(self): return self.with_table_prefix(MySQLSchemaGrammar(self)) def get_schema_manager(self): return MySQLSchemaManager(self) def begin_transaction(self): self._connection.autocommit(False) super(MySQLConnection, self).begin_transaction() def commit(self): if self._transactions == 1: self._connection.commit() self._connection.autocommit(True) self._transactions -= 1 def rollback(self): if self._transactions == 1: self._transactions = 0 self._connection.rollback() self._connection.autocommit(True) else: self._transactions -= 1 def _get_cursor_query(self, query, bindings): if not hasattr(self._cursor, '_last_executed') or self._pretending: return super(MySQLConnection, self)._get_cursor_query(query, bindings) if PY2: return decode(self._cursor._last_executed) return self._cursor._last_executed PK!=)orator/connections/postgres_connection.py# -*- coding: utf-8 -*- from __future__ import division from ..utils import PY2 from .connection import Connection, run from ..query.grammars.postgres_grammar import PostgresQueryGrammar from ..query.processors.postgres_processor import PostgresQueryProcessor from ..schema.grammars import PostgresSchemaGrammar from ..dbal.postgres_schema_manager import PostgresSchemaManager class PostgresConnection(Connection): name = 'pgsql' def get_default_query_grammar(self): return PostgresQueryGrammar(marker=self._marker) def get_default_post_processor(self): return PostgresQueryProcessor() def get_default_schema_grammar(self): return self.with_table_prefix(PostgresSchemaGrammar(self)) def get_schema_manager(self): return PostgresSchemaManager(self) @run def statement(self, query, bindings=None): if self.pretending(): return True bindings = self.prepare_bindings(bindings) self._new_cursor().execute(query, bindings) return True def begin_transaction(self): self._connection.autocommit = False super(PostgresConnection, self).begin_transaction() def commit(self): if self._transactions == 1: self._connection.commit() self._connection.autocommit = True self._transactions -= 1 def rollback(self): if self._transactions == 1: self._transactions = 0 self._connection.rollback() self._connection.autocommit = True else: self._transactions -= 1 def _get_cursor_query(self, query, bindings): if self._pretending: if PY2: return self._cursor.mogrify(query, bindings) return self._cursor.mogrify(query, bindings).decode() if not hasattr(self._cursor, 'query'): return super(PostgresConnection, self)._get_cursor_query(query, bindings) if PY2: return self._cursor.query return self._cursor.query.decode() PK!g5v44'orator/connections/sqlite_connection.py# -*- coding: utf-8 -*- from ..utils import PY2, decode from .connection import Connection from ..query.processors.sqlite_processor import SQLiteQueryProcessor from ..query.grammars.sqlite_grammar import SQLiteQueryGrammar from ..schema.grammars.sqlite_grammar import SQLiteSchemaGrammar from ..dbal.sqlite_schema_manager import SQLiteSchemaManager class SQLiteConnection(Connection): name = 'sqlite' def get_default_query_grammar(self): return self.with_table_prefix(SQLiteQueryGrammar()) def get_default_post_processor(self): return SQLiteQueryProcessor() def get_default_schema_grammar(self): return self.with_table_prefix(SQLiteSchemaGrammar(self)) def get_schema_manager(self): return SQLiteSchemaManager(self) def begin_transaction(self): self._connection.isolation_level = 'DEFERRED' super(SQLiteConnection, self).begin_transaction() def commit(self): if self._transactions == 1: self._connection.commit() self._connection.isolation_level = None self._transactions -= 1 def rollback(self): if self._transactions == 1: self._transactions = 0 self._connection.rollback() self._connection.isolation_level = None else: self._transactions -= 1 def prepare_bindings(self, bindings): bindings = super(SQLiteConnection, self).prepare_bindings(bindings) if PY2: return map(lambda x: decode(x) if isinstance(x, str) else x, bindings) return bindings PK!jE,orator/connectors/__init__.py# -*- coding: utf-8 -*- from .connector import Connector from .mysql_connector import MySQLConnector from .postgres_connector import PostgresConnector from .sqlite_connector import SQLiteConnector PK!Cױ{ { 'orator/connectors/connection_factory.py# -*- coding: utf-8 -*- import random from ..exceptions import ArgumentError from ..exceptions.connectors import UnsupportedDriver from .mysql_connector import MySQLConnector from .postgres_connector import PostgresConnector from .sqlite_connector import SQLiteConnector from ..connections import ( MySQLConnection, PostgresConnection, SQLiteConnection ) class ConnectionFactory(object): CONNECTORS = { 'sqlite': SQLiteConnector, 'mysql': MySQLConnector, 'postgres': PostgresConnector, 'pgsql': PostgresConnector } CONNECTIONS = { 'sqlite': SQLiteConnection, 'mysql': MySQLConnection, 'postgres': PostgresConnection, 'pgsql': PostgresConnection } def make(self, config, name=None): if 'read' in config: return self._create_read_write_connection(config) return self._create_single_connection(config) def _create_single_connection(self, config): conn = self.create_connector(config).connect(config) return self._create_connection( config['driver'], conn, config['database'], config.get('prefix', ''), config ) def _create_read_write_connection(self, config): connection = self._create_single_connection(self._get_write_config(config)) connection.set_read_connection(self._create_read_connection(config)) return connection def _create_read_connection(self, config): read_config = self._get_read_config(config) return self.create_connector(read_config).connect(read_config) def _get_read_config(self, config): read_config = self._get_read_write_config(config, 'read') return self._merge_read_write_config(config, read_config) def _get_write_config(self, config): write_config = self._get_read_write_config(config, 'write') return self._merge_read_write_config(config, write_config) def _get_read_write_config(self, config, type): if config.get(type, []): return random.choice(config[type]) return config[type] def _merge_read_write_config(self, config, merge): config = config.copy() config.update(merge) del config['read'] del config['write'] return config def create_connector(self, config): if 'driver' not in config: raise ArgumentError('A driver must be specified') driver = config['driver'] if driver not in self.CONNECTORS: raise UnsupportedDriver(driver) return self.CONNECTORS[driver](driver) @classmethod def register_connector(cls, name, connector): cls.CONNECTORS[connector] = connector @classmethod def register_connection(cls, name, connection): cls.CONNECTIONS[name] = connection def _create_connection(self, driver, connection, database, prefix='', config=None): if config is None: config = {} if driver not in self.CONNECTIONS: raise UnsupportedDriver(driver) return self.CONNECTIONS[driver](connection, database, prefix, config) PK!0 0 orator/connectors/connector.py# -*- coding: utf-8 -*- from ..dbal.exceptions import InvalidPlatformSpecified from ..exceptions.connectors import MissingPackage class Connector(object): RESERVED_KEYWORDS = [ 'log_queries', 'driver', 'prefix', 'name' ] SUPPORTED_PACKAGES = [] def __init__(self, driver=None): if self.get_api() is None: raise MissingPackage(driver, self.SUPPORTED_PACKAGES) self._connection = None self._platform = None self._params = {} def get_api(self): raise NotImplementedError() def get_config(self, config): default_config = self.get_default_config() config = {x: config[x] for x in config if x not in self.RESERVED_KEYWORDS} default_config.update(config) return default_config def get_default_config(self): return {} def connect(self, config): self._params = self.get_config(config) self._connection = self._do_connect(config) return self def _do_connect(self, config): return self.get_api().connect(**self.get_config(config)) def get_params(self): return self._params def get_database(self): return self._params.get('database') def get_host(self): return self._params.get('host') def get_user(self): return self._params.get('user') def get_password(self): return self._params.get('password') def get_database_platform(self): if self._platform is None: self._detect_database_platform() return self._platform def _detect_database_platform(self): """ Detects and sets the database platform. Evaluates custom platform class and version in order to set the correct platform. :raises InvalidPlatformSpecified: if an invalid platform was specified for this connection. """ version = self._get_database_platform_version() if version is not None: self._platform = self._create_database_platform_for_version(version) else: self._platform = self.get_dbal_platform() def _get_database_platform_version(self): """ Returns the version of the related platform if applicable. Returns None if either the connector is not capable to create version specific platform instances, no explicit server version was specified or the underlying driver connection cannot determine the platform version without having to query it (performance reasons). :rtype: str or None """ # Connector does not support version specific platforms. if not self.is_version_aware(): return None return self.get_server_version() def _create_database_platform_for_version(self, version): raise NotImplementedError() def get_dbal_platform(self): raise NotImplementedError() def is_version_aware(self): return True def get_server_version(self): return None def __getattr__(self, item): return getattr(self._connection, item) PK!_FHH$orator/connectors/mysql_connector.py# -*- coding: utf-8 -*- import re from pendulum import Pendulum, Date try: import MySQLdb as mysql # Fix for understanding Pendulum object import MySQLdb.converters MySQLdb.converters.conversions[Pendulum] = MySQLdb.converters.DateTime2literal MySQLdb.converters.conversions[Date] = MySQLdb.converters.Thing2Literal from MySQLdb.cursors import DictCursor as cursor_class keys_fix = { 'password': 'passwd', 'database': 'db' } except ImportError as e: try: import pymysql as mysql # Fix for understanding Pendulum object import pymysql.converters pymysql.converters.conversions[Pendulum] = pymysql.converters.escape_datetime pymysql.converters.conversions[Date] = pymysql.converters.escape_date from pymysql.cursors import DictCursor as cursor_class keys_fix = {} except ImportError as e: mysql = None cursor_class = object from ..dbal.platforms import MySQLPlatform, MySQL57Platform from .connector import Connector from ..utils.qmarker import qmark, denullify from ..utils.helpers import serialize class Record(dict): def __getattr__(self, item): try: return self[item] except KeyError: raise AttributeError(item) def serialize(self): return serialize(self) class BaseDictCursor(cursor_class): def _fetch_row(self, size=1): # Overridden for mysqclient if not self._result: return () rows = self._result.fetch_row(size, self._fetch_type) return tuple(Record(r) for r in rows) def _conv_row(self, row): # Overridden for pymysql return Record(super(BaseDictCursor, self)._conv_row(row)) class DictCursor(BaseDictCursor): def execute(self, query, args=None): query = qmark(query) return super(DictCursor, self).execute(query, args) def executemany(self, query, args): query = qmark(query) return super(DictCursor, self).executemany( query, denullify(args) ) class MySQLConnector(Connector): RESERVED_KEYWORDS = [ 'log_queries', 'driver', 'prefix', 'engine', 'collation', 'name', 'use_qmark' ] SUPPORTED_PACKAGES = ['PyMySQL', 'mysqlclient'] def _do_connect(self, config): config = dict(config.items()) for key, value in keys_fix.items(): config[value] = config[key] del config[key] config['autocommit'] = True config['cursorclass'] = self.get_cursor_class(config) return self.get_api().connect(**self.get_config(config)) def get_default_config(self): return { 'charset': 'utf8', 'use_unicode': True } def get_cursor_class(self, config): if config.get('use_qmark'): return DictCursor return BaseDictCursor def get_api(self): return mysql def get_server_version(self): version = self._connection.get_server_info() version_parts = re.match('^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?', version) major = int(version_parts.group('major')) minor = version_parts.group('minor') or 0 patch = version_parts.group('patch') or 0 minor, patch = int(minor), int(patch) server_version = (major, minor, patch, '') if 'mariadb' in version.lower(): server_version = (major, minor, patch, 'mariadb') return server_version def _create_database_platform_for_version(self, version): major, minor, _, extra = version if extra == 'mariadb': return self.get_dbal_platform() if (major, minor) >= (5, 7): return MySQL57Platform() return self.get_dbal_platform() def get_dbal_platform(self): return MySQLPlatform() PK!m5 'orator/connectors/postgres_connector.py# -*- coding: utf-8 -*- try: import psycopg2 import psycopg2.extras from psycopg2 import extensions connection_class = psycopg2.extras.DictConnection cursor_class = psycopg2.extras.DictCursor row_class = psycopg2.extras.DictRow except ImportError: psycopg2 = None connection_class = object cursor_class = object row_class = object from ..dbal.platforms import PostgresPlatform from .connector import Connector from ..utils.qmarker import qmark, denullify from ..utils.helpers import serialize class BaseDictConnection(connection_class): def cursor(self, *args, **kwargs): kwargs.setdefault('cursor_factory', BaseDictCursor) return super(BaseDictConnection, self).cursor(*args, **kwargs) class DictConnection(BaseDictConnection): def cursor(self, *args, **kwargs): kwargs.setdefault('cursor_factory', DictCursor) return super(DictConnection, self).cursor(*args, **kwargs) class BaseDictCursor(cursor_class): def __init__(self, *args, **kwargs): kwargs['row_factory'] = DictRow super(cursor_class, self).__init__(*args, **kwargs) self._prefetch = 1 class DictCursor(BaseDictCursor): def execute(self, query, vars=None): query = qmark(query) return super(DictCursor, self).execute(query, vars) def executemany(self, query, args_seq): query = qmark(query) return super(DictCursor, self).executemany( query, denullify(args_seq)) class DictRow(row_class): def __getattr__(self, item): try: return self[item] except KeyError: raise AttributeError(item) def serialize(self): serialized = {} for column, index in self._index.items(): serialized[column] = list.__getitem__(self, index) return serialize(serialized) class PostgresConnector(Connector): RESERVED_KEYWORDS = [ 'log_queries', 'driver', 'prefix', 'name', 'register_unicode', 'use_qmark' ] SUPPORTED_PACKAGES = ['psycopg2'] def _do_connect(self, config): connection = self.get_api().connect( connection_factory=self.get_connection_class(config), **self.get_config(config) ) if config.get('use_unicode', True): extensions.register_type(extensions.UNICODE, connection) extensions.register_type(extensions.UNICODEARRAY, connection) connection.autocommit = True return connection def get_connection_class(self, config): if config.get('use_qmark'): return DictConnection return BaseDictConnection def get_api(self): return psycopg2 @property def autocommit(self): return self._connection.autocommit @autocommit.setter def autocommit(self, value): self._connection.autocommit = value def get_dbal_platform(self): return PostgresPlatform() def is_version_aware(self): return False def get_server_version(self): int_version = self._connection.server_version major = int_version // 10000 minor = int_version // 100 % 100 fix = int_version % 10 return major, minor, fix, '' PK!'%orator/connectors/sqlite_connector.py# -*- coding: utf-8 -*- from pendulum import Pendulum, Date try: import sqlite3 from sqlite3 import register_adapter register_adapter(Pendulum, lambda val: val.isoformat(' ')) register_adapter(Date, lambda val: val.isoformat()) except ImportError: sqlite3 = None from ..dbal.platforms import SQLitePlatform from ..utils.helpers import serialize from .connector import Connector class DictCursor(dict): def __init__(self, cursor, row): self.dict = {} self.cursor = cursor for idx, col in enumerate(cursor.description): self.dict[col[0]] = row[idx] super(DictCursor, self).__init__(self.dict) def __getattr__(self, item): try: return self[item] except KeyError: return getattr(self.cursor, item) def serialize(self): return serialize(self) class SQLiteConnector(Connector): RESERVED_KEYWORDS = [ 'log_queries', 'driver', 'prefix', 'name', 'foreign_keys', 'use_qmark' ] def _do_connect(self, config): connection = self.get_api().connect(**self.get_config(config)) connection.isolation_level = None connection.row_factory = DictCursor # We activate foreign keys support by default if config.get('foreign_keys', True): connection.execute("PRAGMA foreign_keys = ON") return connection def get_api(self): return sqlite3 @property def isolation_level(self): return self._connection.isolation_level @isolation_level.setter def isolation_level(self, value): self._connection.isolation_level = value def get_dbal_platform(self): return SQLitePlatform() def is_version_aware(self): return False def get_server_version(self): sql = 'select sqlite_version() AS sqlite_version' rows = self._connection.execute(sql).fetchall() version = rows[0]['sqlite_version'] return tuple(version.split('.')[:3] + ['']) PK!WZorator/database_manager.py# -*- coding: utf-8 -*- import threading import logging from .connections.connection_resolver_interface import ConnectionResolverInterface from .connectors.connection_factory import ConnectionFactory from .exceptions import ArgumentError logger = logging.getLogger('orator.database_manager') class BaseDatabaseManager(ConnectionResolverInterface): def __init__(self, config, factory=ConnectionFactory()): """ :param config: The connections configuration :type config: dict :param factory: A connection factory :type factory: ConnectionFactory """ self._config = config self._factory = factory self._connections = {} self._extensions = {} def connection(self, name=None): """ Get a database connection instance :param name: The connection name :type name: str :return: A Connection instance :rtype: orator.connections.connection.Connection """ name, type = self._parse_connection_name(name) if name not in self._connections: logger.debug('Initiating connection %s' % name) connection = self._make_connection(name) self._set_connection_for_type(connection, type) self._connections[name] = self._prepare(connection) return self._connections[name] def _parse_connection_name(self, name): """ Parse the connection into a tuple of the name and read / write type :param name: The name of the connection :type name: str :return: A tuple of the name and read / write type :rtype: tuple """ if name is None: name = self.get_default_connection() if name.endswith(('::read', '::write')): return name.split('::', 1) return name, None def purge(self, name=None): """ Disconnect from the given database and remove from local cache :param name: The name of the connection :type name: str :rtype: None """ if name is None: name = self.get_default_connection() self.disconnect(name) if name in self._connections: del self._connections[name] def disconnect(self, name=None): if name is None: name = self.get_default_connection() logger.debug('Disconnecting %s' % name) if name in self._connections: self._connections[name].disconnect() def reconnect(self, name=None): if name is None: name = self.get_default_connection() logger.debug('Reconnecting %s' % name) self.disconnect(name) if name not in self._connections: return self.connection(name) return self._refresh_api_connections(name) def _refresh_api_connections(self, name): logger.debug('Refreshing api connections for %s' % name) fresh = self._make_connection(name) return self._connections[name]\ .set_connection(fresh.get_connection())\ .set_read_connection(fresh.get_read_connection()) def _make_connection(self, name): logger.debug('Making connection for %s' % name) config = self._get_config(name) if 'name' not in config: config['name'] = name if name in self._extensions: return self._extensions[name](config, name) driver = config['driver'] if driver in self._extensions: return self._extensions[driver](config, name) return self._factory.make(config, name) def _prepare(self, connection): logger.debug('Preparing connection %s' % connection.get_name()) def reconnector(connection_): self.reconnect(connection_.get_name()) connection.set_reconnector(reconnector) return connection def _set_connection_for_type(self, connection, type): if type == 'read': connection.set_connection(connection.get_read_api()) elif type == 'write': connection.set_read_connection(connection.get_api()) return connection def _get_config(self, name): if name is None: name = self.get_default_connection() connections = self._config config = connections.get(name) if not config: raise ArgumentError('Database [%s] not configured' % name) return config def get_default_connection(self): if len(self._config) == 1: return list(self._config.keys())[0] return self._config['default'] def set_default_connection(self, name): if name is not None: self._config['default'] = name def extend(self, name, resolver): self._extensions[name] = resolver def get_connections(self): return self._connections def __getattr__(self, item): return getattr(self.connection(), item) class DatabaseManager(BaseDatabaseManager, threading.local): pass PK!orator/dbal/__init__.py# -*- coding: utf-8 -*- PK!_; ; orator/dbal/abstract_asset.py# -*- coding: utf-8 -*- import re import binascii from ..utils import encode class AbstractAsset(object): _name = None _namespace = None _quoted = False def _set_name(self, name): """ Sets the name of this asset. :param name: The name of the asset :type name: str """ if self._is_identifier_quoted(name): self._quoted = True name = self._trim_quotes(name) if '.' in name: parts = name.split('.', 1) self._namespace = parts[0] name = parts[1] self._name = name def _is_in_default_namespace(self, default_namespace): return self._namespace == default_namespace or self._namespace is None def get_namespace_name(self): return self._namespace def get_shortest_name(self, default_namespace): shortest_name = self.get_name() if self._namespace == default_namespace: shortest_name = self._name return shortest_name.lower() def get_full_qualified_name(self, default_namespace): name = self.get_name() if not self._namespace: name = default_namespace + '.' + name return name.lower() def is_quoted(self): return self._quoted def _is_identifier_quoted(self, identifier): return len(identifier) > 0\ and (identifier[0] == '`' or identifier[0] == '"' or identifier[0] == '[') def _trim_quotes(self, identifier): return re.sub('[`"\[\]]', '', identifier) def get_name(self): if self._namespace: return self._namespace + '.' + self._name return self._name def get_quoted_name(self, platform): keywords = platform.get_reserved_keywords_list() parts = self.get_name().split('.') for k, v in enumerate(parts): if self._quoted or keywords.is_keyword(v): parts[k] = platform.quote_identifier(v) return '.'.join(parts) def _generate_identifier_name(self, columns, prefix='', max_size=30): """ Generates an identifier from a list of column names obeying a certain string length. """ hash = '' for column in columns: hash += '%x' % binascii.crc32(encode(str(column))) return (prefix + '_' + hash)[:max_size] PK!JI"6I I orator/dbal/column.py# -*- coding: utf-8 -*- from .abstract_asset import AbstractAsset from ..utils import basestring class Column(AbstractAsset): def __init__(self, name, type, options=None): self._set_name(name) self._type = type self._length = None self._precision = 10 self._scale = 0 self._unsigned = False self._fixed = False self._notnull = True self._default = None self._autoincrement = False self._extra = {} self._platform_options = {} self.set_options(options or {}) def set_options(self, options): for key, value in options.items(): method = 'set_%s' % key if hasattr(self, method): getattr(self, method)(value) return self def set_platform_options(self, platform_options): self._platform_options = platform_options return self def set_platform_option(self, name, value): self._platform_options[name] = value return self def get_platform_options(self): return self._platform_options def has_platform_option(self, option): return option in self._platform_options def get_platform_option(self, option): return self._platform_options[option] def set_length(self, length): if length is not None: self._length = int(length) else: self._length = None return self def set_precision(self, precision): if precision is None or isinstance(precision, basestring) and not precision.isdigit(): precision = 10 self._precision = int(precision) return self def set_scale(self, scale): if scale is None or isinstance(scale, basestring) and not scale.isdigit(): scale = 0 self._scale = int(scale) return self def set_unsigned(self, unsigned): self._unsigned = bool(unsigned) def set_fixed(self, fixed): self._fixed = bool(fixed) def set_notnull(self, notnull): self._notnull = bool(notnull) def set_default(self, default): self._default = default def set_autoincrement(self, flag): self._autoincrement = flag return self def set_type(self, type): self._type = type def set_extra(self, extra, key=None): if key: self._extra[key] = extra else: self._extra = extra def get_name(self): return self._name def get_type(self): return self._type def get_extra(self, name=None): if name is not None: return self._extra[name] return self._extra def get_autoincrement(self): return self._autoincrement def get_notnull(self): return self._notnull def get_default(self): return self._default def to_dict(self): d = { 'name': self._name, 'type': self._type, 'default': self._default, 'notnull': self._notnull, 'length': self._length, 'precision': self._precision, 'scale': self._scale, 'fixed': self._fixed, 'unsigned': self._unsigned, 'autoincrement': self._autoincrement, 'extra': self._extra } d.update(self._platform_options) return d PK!bn͜orator/dbal/column_diff.py# -*- coding: utf-8 -*- from .identifier import Identifier class ColumnDiff(object): def __init__(self, old_column_name, column, changed_properties=None, from_column=None): self.old_column_name = old_column_name self.column = column self.changed_properties = changed_properties self.from_column = from_column def has_changed(self, property_name): return property_name in self.changed_properties def get_old_column_name(self): return Identifier(self.old_column_name) PK!v^*^*orator/dbal/comparator.py# -*- coding: utf-8 -*- from collections import OrderedDict from .table_diff import TableDiff from .column_diff import ColumnDiff from .index import Index from .foreign_key_constraint import ForeignKeyConstraint class Comparator(object): """ Compares two Schemas and return an instance of SchemaDiff. """ def diff_table(self, table1, table2): """ Returns the difference between the tables table1 and table2. :type table1: Table :type table2: Table :rtype: TableDiff """ changes = 0 table_differences = TableDiff(table1.get_name()) table_differences.from_table = table1 table1_columns = table1.get_columns() table2_columns = table2.get_columns() # See if all the fields in table1 exist in table2 for column_name, column in table2_columns.items(): if not table1.has_column(column_name): table_differences.added_columns[column_name] = column changes += 1 # See if there are any removed fields in table2 for column_name, column in table1_columns.items(): if not table2.has_column(column_name): table_differences.removed_columns[column_name] = column changes += 1 continue # See if column has changed properties in table2 changed_properties = self.diff_column(column, table2.get_column(column_name)) if changed_properties: column_diff = ColumnDiff(column.get_name(), table2.get_column(column_name), changed_properties) column_diff.from_column = column table_differences.changed_columns[column.get_name()] = column_diff changes += 1 self.detect_column_renamings(table_differences) table1_indexes = table1.get_indexes() table2_indexes = table2.get_indexes() # See if all the fields in table1 exist in table2 for index_name, index in table2_indexes.items(): if (index.is_primary() and not table1.has_primary_key()) or table1.has_index(index_name): continue table_differences.added_indexes[index_name] = index changes += 1 # See if there are any removed fields in table2 for index_name, index in table1_indexes.items(): if (index.is_primary() and not table2.has_primary_key())\ or (not index.is_primary() and not table2.has_index(index_name)): table_differences.removed_indexes[index_name] = index changes += 1 continue # See if index has changed in table 2 if index.is_primary(): table2_index = table2.get_primary_key() else: table2_index = table2.get_index(index_name) if self.diff_index(index, table2_index): table_differences.changed_indexes[index_name] = index changes += 1 self.detect_index_renamings(table_differences) from_fkeys = OrderedDict([(k, v) for k, v in table1.get_foreign_keys().items()]) to_fkeys = OrderedDict([(k, v) for k, v in table2.get_foreign_keys().items()]) for key1, constraint1 in table1.get_foreign_keys().items(): for key2, constraint2 in table2.get_foreign_keys().items(): if self.diff_foreign_key(constraint1, constraint2) is False: del from_fkeys[key1] del to_fkeys[key2] else: if constraint1.get_name().lower() == constraint2.get_name().lower(): table_differences.changed_foreign_keys.append(constraint2) changes += 1 del from_fkeys[key1] del to_fkeys[key2] for constraint1 in from_fkeys.values(): table_differences.removed_foreign_keys.append(constraint1) changes += 1 for constraint2 in to_fkeys.values(): table_differences.added_foreign_keys.append(constraint2) changes += 1 if changes: return table_differences return False def detect_column_renamings(self, table_differences): """ Try to find columns that only changed their names. :type table_differences: TableDiff """ rename_candidates = {} for added_column_name, added_column in table_differences.added_columns.items(): for removed_column in table_differences.removed_columns.values(): if len(self.diff_column(added_column, removed_column)) == 0: if added_column.get_name() not in rename_candidates: rename_candidates[added_column.get_name()] = [] rename_candidates[added_column.get_name()].append((removed_column, added_column, added_column_name)) for candidate_columns in rename_candidates.values(): if len(candidate_columns) == 1: removed_column, added_column, _ = candidate_columns[0] removed_column_name = removed_column.get_name().lower() added_column_name = added_column.get_name().lower() if removed_column_name not in table_differences.renamed_columns: table_differences.renamed_columns[removed_column_name] = added_column del table_differences.added_columns[added_column_name] del table_differences.removed_columns[removed_column_name] def detect_index_renamings(self, table_differences): """ Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop however ambiguities between different possibilities should not lead to renaming at all. :type table_differences: TableDiff """ rename_candidates = OrderedDict() # Gather possible rename candidates by comparing # each added and removed index based on semantics. for added_index_name, added_index in table_differences.added_indexes.items(): for removed_index in table_differences.removed_indexes.values(): if not self.diff_index(added_index, removed_index): if added_index.get_name() not in rename_candidates: rename_candidates[added_index.get_name()] = [] rename_candidates[added_index.get_name()].append((removed_index, added_index, added_index_name)) for candidate_indexes in rename_candidates.values(): # If the current rename candidate contains exactly one semantically equal index, # we can safely rename it. # Otherwise it is unclear if a rename action is really intended, # therefore we let those ambiguous indexes be added/dropped. if len(candidate_indexes) == 1: removed_index, added_index, _ = candidate_indexes[0] removed_index_name = removed_index.get_name().lower() added_index_name = added_index.get_name().lower() if not removed_index_name in table_differences.renamed_indexes: table_differences.renamed_indexes[removed_index_name] = added_index del table_differences.added_indexes[added_index_name] del table_differences.removed_indexes[removed_index_name] def diff_foreign_key(self, key1, key2): """ :type key1: ForeignKeyConstraint :type key2: ForeignKeyConstraint :rtype: bool """ key1_unquoted_local_columns = [c.lower() for c in key1.get_unquoted_local_columns()] key2_unquoted_local_columns = [c.lower() for c in key2.get_unquoted_local_columns()] if key1_unquoted_local_columns != key2_unquoted_local_columns: return True key1_unquoted_foreign_columns = [c.lower() for c in key1.get_unquoted_foreign_columns()] key2_unquoted_foreign_columns = [c.lower() for c in key2.get_unquoted_foreign_columns()] if key1_unquoted_foreign_columns != key2_unquoted_foreign_columns: return True if key1.get_unqualified_foreign_table_name() != key2.get_unqualified_foreign_table_name(): return True if key1.on_update() != key2.on_update(): return True if key1.on_delete() != key2.on_delete(): return True return False def diff_column(self, column1, column2): """ Returns the difference between column1 and column2 :type column1: orator.dbal.column.Column :type column2: orator.dbal.column.Column :rtype: list """ properties1 = column1.to_dict() properties2 = column2.to_dict() changed_properties = [] for prop in ['type', 'notnull', 'unsigned', 'autoincrement']: if properties1[prop] != properties2[prop]: changed_properties.append(prop) if properties1['default'] != properties2['default']\ or (properties1['default'] is None and properties2['default'] is not None)\ or (properties2['default'] is None and properties1['default'] is not None): changed_properties.append('default') if properties1['type'] == 'string' and properties1['type'] != 'guid'\ or properties1['type'] in ['binary', 'blob']: length1 = properties1['length'] or 255 length2 = properties2['length'] or 255 if length1 != length2: changed_properties.append('length') if properties1['fixed'] != properties2['fixed']: changed_properties.append('fixed') elif properties1['type'] in ['decimal', 'float', 'double precision']: precision1 = properties1['precision'] or 10 precision2 = properties2['precision'] or 10 if precision1 != precision2: changed_properties.append('precision') if properties1['scale'] != properties2['scale']: changed_properties.append('scale') return list(set(changed_properties)) def diff_index(self, index1, index2): """ Finds the difference between the indexes index1 and index2. Compares index1 with index2 and returns True if there are any differences or False in case there are no differences. :type index1: Index :type index2: Index :rtype: bool """ if index1.is_fullfilled_by(index2) and index2.is_fullfilled_by(index1): return False return True PK!{Jk55"orator/dbal/exceptions/__init__.py# -*- coding: utf-8 -*- class DBALException(Exception): pass class InvalidPlatformSpecified(DBALException): def __init__(self, index_name, table_name): message = 'Invalid "platform" option specified, need to give an instance of dbal.platforms.Platform' super(InvalidPlatformSpecified, self).__init__(message) class SchemaException(DBALException): pass class IndexDoesNotExist(SchemaException): def __init__(self, index_name, table_name): message = 'Index "%s" does not exist on table "%s".' % (index_name, table_name) super(IndexDoesNotExist, self).__init__(message) class IndexAlreadyExists(SchemaException): def __init__(self, index_name, table_name): message = 'An index with name "%s" already exists on table "%s".' % (index_name, table_name) super(IndexAlreadyExists, self).__init__(message) class IndexNameInvalid(SchemaException): def __init__(self, index_name): message = 'Invalid index name "%s" given, has to be [a-zA-Z0-9_]' % index_name super(IndexNameInvalid, self).__init__(message) class ColumnDoesNotExist(SchemaException): def __init__(self, column, table_name): message = 'Column "%s" does not exist on table "%s".' % (column, table_name) super(ColumnDoesNotExist, self).__init__(message) class ColumnAlreadyExists(SchemaException): def __init__(self, column, table_name): message = 'An column with name "%s" already exists on table "%s".' % (column, table_name) super(ColumnAlreadyExists, self).__init__(message) class ForeignKeyDoesNotExist(SchemaException): def __init__(self, constraint, table_name): message = 'Foreign key "%s" does not exist on table "%s".' % (constraint, table_name) super(ForeignKeyDoesNotExist, self).__init__(message) PK!E%orator/dbal/foreign_key_constraint.py# -*- coding: utf-8 -*- from collections import OrderedDict from .abstract_asset import AbstractAsset from .identifier import Identifier class ForeignKeyConstraint(AbstractAsset): """ An abstraction class for a foreign key constraint. """ def __init__(self, local_column_names, foreign_table_name, foreign_column_names, name=None, options=None): """ Constructor. :param local_column_names: Names of the referencing table columns. :type local_column_names: list :param foreign_table_name: Referenced table. :type foreign_table_name: str :param foreign_column_names: Names of the referenced table columns. :type foreign_column_names: list :param name: Name of the foreign key constraint. :type name: str or None :param options: Options associated with the foreign key constraint. :type options: dict or None """ from .table import Table self._set_name(name) self._local_table = None self._local_column_names = OrderedDict() if local_column_names: for column_name in local_column_names: self._local_column_names[column_name] = Identifier(column_name) if isinstance(foreign_table_name, Table): self._foreign_table_name = foreign_table_name else: self._foreign_table_name = Identifier(foreign_table_name) self._foreign_column_names = OrderedDict() if foreign_column_names: for column_name in foreign_column_names: self._foreign_column_names[column_name] = Identifier(column_name) self._options = options or {} def get_local_table_name(self): """ Returns the name of the referencing table the foreign key constraint is associated with. :rtype: str """ self._local_table.get_name() def set_local_table(self, table): """ Sets the Table instance of the referencing table the foreign key constraint is associated with. :param table: Instance of the referencing table. :type table: Table """ self._local_table = table def get_local_table(self): """ :rtype: Table """ return self._local_table def get_local_columns(self): """ Returns the names of the referencing table columns the foreign key constraint is associated with. :rtype: list """ return list(self._local_column_names.keys()) def get_quoted_local_columns(self, platform): """ Returns the quoted representation of the referencing table column names the foreign key constraint is associated with. But only if they were defined with one or the referencing table column name is a keyword reserved by the platform. Otherwise the plain unquoted value as inserted is returned. :param platform: The platform to use for quotation. :type platform: Platform :rtype: list """ columns = [] for column in self._local_column_names.values(): columns.append(column.get_quoted_name(platform)) return columns def get_unquoted_local_columns(self): """ Returns unquoted representation of local table column names for comparison with other FK. :rtype: list """ return list(map(self._trim_quotes, self.get_local_columns())) def get_columns(self): return self.get_local_columns() def get_quoted_columns(self, platform): """ Returns the quoted representation of the referencing table column names the foreign key constraint is associated with. But only if they were defined with one or the referencing table column name is a keyword reserved by the platform. Otherwise the plain unquoted value as inserted is returned. :param platform: The platform to use for quotation. :type platform: Platform :rtype: list """ return self.get_quoted_local_columns(platform) def get_foreign_table_name(self): """ Returns the name of the referenced table the foreign key constraint is associated with. :rtype: str """ return self._foreign_table_name.get_name() def get_unqualified_foreign_table_name(self): """ Returns the non-schema qualified foreign table name. :rtype: str """ parts = self.get_foreign_table_name().split('.') return parts[-1].lower() def get_quoted_foreign_table_name(self, platform): """ Returns the quoted representation of the referenced table name the foreign key constraint is associated with. But only if it was defined with one or the referenced table name is a keyword reserved by the platform. Otherwise the plain unquoted value as inserted is returned. :param platform: The platform to use for quotation. :type platform: Platform :rtype: str """ return self._foreign_table_name.get_quoted_name(platform) def get_foreign_columns(self): """ Returns the names of the referenced table columns the foreign key constraint is associated with. :rtype: list """ return list(self._foreign_column_names.keys()) def get_quoted_foreign_columns(self, platform): """ Returns the quoted representation of the referenced table column names the foreign key constraint is associated with. But only if they were defined with one or the referenced table column name is a keyword reserved by the platform. Otherwise the plain unquoted value as inserted is returned. :param platform: The platform to use for quotation. :type platform: Platform :rtype: list """ columns = [] for column in self._foreign_column_names.values(): columns.append(column.get_quoted_name(platform)) return columns def get_unquoted_foreign_columns(self): """ Returns unquoted representation of foreign table column names for comparison with other FK. :rtype: list """ return list(map(self._trim_quotes, self.get_foreign_columns())) def has_option(self, name): return name in self._options def get_option(self, name): return self._options[name] def get_options(self): return self._options def on_update(self): """ Returns the referential action for UPDATE operations on the referenced table the foreign key constraint is associated with. :rtype: str or None """ return self._on_event('on_update') def on_delete(self): """ Returns the referential action for DELETE operations on the referenced table the foreign key constraint is associated with. :rtype: str or None """ return self._on_event('on_delete') def _on_event(self, event): """ Returns the referential action for a given database operation on the referenced table the foreign key constraint is associated with. :param event: Name of the database operation/event to return the referential action for. :type event: str :rtype: str or None """ if self.has_option(event): on_event = self.get_option(event).upper() if on_event not in ['NO ACTION', 'RESTRICT']: return on_event return False def intersects_index_columns(self, index): """ Checks whether this foreign key constraint intersects the given index columns. Returns `true` if at least one of this foreign key's local columns matches one of the given index's columns, `false` otherwise. :param index: The index to be checked against. :type index: Index :rtype: bool """ PK!sήorator/dbal/identifier.py# -*- coding: utf-8 -*- from .abstract_asset import AbstractAsset class Identifier(AbstractAsset): def __init__(self, identifier): self._set_name(identifier) PK!C88orator/dbal/index.py# -*- coding: utf-8 -*- from collections import OrderedDict from .abstract_asset import AbstractAsset from .identifier import Identifier class Index(AbstractAsset): """ An abstraction class for an index. """ def __init__(self, name, columns, is_unique=False, is_primary=False, flags=None, options=None): """ Constructor. :param name: The index name :type name: str :param columns: The index columns :type columns: list :param is_unique: Whether the index is unique or not :type is_unique: bool :param is_primary: Whether the index is primary or not :type is_primary: bool :param flags: The index flags :type: dict """ is_unique = is_unique or is_primary self._set_name(name) self._is_unique = is_unique self._is_primary = is_primary self._options = options or {} self._columns = OrderedDict() self._flags = OrderedDict() for column in columns: self._add_column(column) flags = flags or OrderedDict() for flag in flags: self.add_flag(flag) def _add_column(self, column): """ Adds a new column. :param column: The column to add :type column: str """ self._columns[column] = Identifier(column) def get_columns(self): """ :rtype: list """ return list(self._columns.keys()) def get_quoted_columns(self, platform): """ Returns the quoted representation of the column names the constraint is associated with. But only if they were defined with one or a column name is a keyword reserved by the platform. Otherwise the plain unquoted value as inserted is returned. :param platform: The platform to use for quotation. :type platform: Platform :rtype: list """ columns = [] for column in self._columns.values(): columns.append(column.get_quoted_name(platform)) return columns def get_unquoted_columns(self): return list(map(self._trim_quotes, self.get_columns())) def is_simple_index(self): """ Is the index neither unique nor primary key? :rtype: bool """ return not self._is_primary and not self._is_unique def is_unique(self): return self._is_unique def is_primary(self): return self._is_primary def has_column_at_position(self, column_name, pos=0): """ :type column_name: str :type pos: int :rtype: bool """ column_name = self._trim_quotes(column_name.lower()) index_columns = [c.lower() for c in self.get_unquoted_columns()] return index_columns.index(column_name) == pos def spans_columns(self, column_names): """ Checks if this index exactly spans the given column names in the correct order. :type column_names: list :rtype: bool """ columns = self.get_columns() number_of_columns = len(columns) same_columns = True for i in range(number_of_columns): column = self._trim_quotes(columns[i].lower()) if i >= len(column_names) or column != self._trim_quotes(column_names[i].lower()): same_columns = False return same_columns def is_fullfilled_by(self, other): """ Checks if the other index already fulfills all the indexing and constraint needs of the current one. :param other: The other index :type other: Index :rtype: bool """ # allow the other index to be equally large only. It being larger is an option # but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) if len(other.get_columns()) != len(self.get_columns()): return False # Check if columns are the same, and even in the same order if not self.spans_columns(other.get_columns()): return False if not self.same_partial_index(other): return False if self.is_simple_index(): # this is a special case: If the current key is neither primary or unique, # any unique or primary key will always have the same effect # for the index and there cannot be any constraint overlaps. # This means a primary or unique index can always fulfill # the requirements of just an index that has no constraints. return True if other.is_primary() != self.is_primary(): return False if other.is_unique() != self.is_unique(): return False return True def same_partial_index(self, other): """ Return whether the two indexes have the same partial index :param other: The other index :type other: Index :rtype: bool """ if (self.has_option('where') and other.has_option('where') and self.get_option('where') == other.get_option('where')): return True if not self.has_option('where') and not other.has_option('where'): return True return False def overrules(self, other): """ Detects if the other index is a non-unique, non primary index that can be overwritten by this one. :param other: The other index :type other: Index :rtype: bool """ if other.is_primary(): return False elif self.is_simple_index() and other.is_unique(): return False same_columns = self.spans_columns(other.get_columns()) if same_columns and (self.is_primary() or self.is_unique()) and self.same_partial_index(other): return True return False def get_flags(self): """ Returns platform specific flags for indexes. :rtype: list """ return list(self._flags.keys()) def add_flag(self, flag): """ Adds Flag for an index that translates to platform specific handling. >>> index.add_flag('CLUSTERED') :type flag: str :rtype: Index """ self._flags[flag.lower()] = True return self def has_flag(self, flag): """ Does this index have a specific flag? :type flag: str :rtype: bool """ return flag.lower() in self._flags def remove_flag(self, flag): """ Removes a flag. :type flag: str """ if self.has_flag(flag): del self._flags[flag.lower()] def has_option(self, name): return name in self._options def get_option(self, name): return self._options[name] def get_options(self): return self._options PK!6Z..#orator/dbal/mysql_schema_manager.py# -*- coding: utf-8 -*- import re from collections import OrderedDict from .column import Column from .foreign_key_constraint import ForeignKeyConstraint from .schema_manager import SchemaManager from .platforms.mysql_platform import MySQLPlatform class MySQLSchemaManager(SchemaManager): def _get_portable_table_column_definition(self, table_column): db_type = table_column['type'].lower() type_match = re.match('(.+)\((.*)\).*', db_type) if type_match: db_type = type_match.group(1) if 'length' in table_column: length = table_column['length'] else: if type_match and type_match.group(2) and ',' not in type_match.group(2): length = int(type_match.group(2)) else: length = 0 fixed = None if 'name' not in table_column: table_column['name'] = '' precision = None scale = None extra = {} type = self._platform.get_type_mapping(db_type) if db_type in ['char', 'binary']: fixed = True elif db_type in ['float', 'double', 'real', 'decimal', 'numeric']: match = re.match('([A-Za-z]+\(([0-9]+),([0-9]+)\))', table_column['type']) if match: precision = match.group(1) scale = match.group(2) length = None elif db_type == 'tinytext': length = MySQLPlatform.LENGTH_LIMIT_TINYTEXT elif db_type == 'text': length = MySQLPlatform.LENGTH_LIMIT_TEXT elif db_type == 'mediumtext': length = MySQLPlatform.LENGTH_LIMIT_MEDIUMTEXT elif db_type == 'tinyblob': length = MySQLPlatform.LENGTH_LIMIT_TINYBLOB elif db_type == 'blob': length = MySQLPlatform.LENGTH_LIMIT_BLOB elif db_type == 'mediumblob': length = MySQLPlatform.LENGTH_LIMIT_MEDIUMBLOB elif db_type in ['tinyint', 'smallint', 'mediumint', 'int', 'bigint', 'year']: length = None elif db_type == 'enum': length = None extra['definition'] = '({})'.format(type_match.group(2)) if length is None or length == 0: length = None options = { 'length': length, 'unsigned': table_column['type'].find('unsigned') != -1, 'fixed': fixed, 'notnull': table_column['null'] != 'YES', 'default': table_column.get('default'), 'precision': None, 'scale': None, 'autoincrement': table_column['extra'].find('auto_increment') != -1, 'extra': extra, } if scale is not None and precision is not None: options['scale'] = scale options['precision'] = precision column = Column(table_column['field'], type, options) if 'collation' in table_column: column.set_platform_option('collation', table_column['collation']) return column def _get_portable_table_indexes_list(self, table_indexes, table_name): new = [] for v in table_indexes: v = dict((k.lower(), value) for k, value in v.items()) if v['key_name'] == 'PRIMARY': v['primary'] = True else: v['primary'] = False if 'FULLTEXT' in v['index_type']: v['flags'] = {'FULLTEXT': True} else: v['flags'] = {'SPATIAL': True} new.append(v) return super(MySQLSchemaManager, self)._get_portable_table_indexes_list(new, table_name) def _get_portable_table_foreign_keys_list(self, table_foreign_keys): foreign_keys = OrderedDict() for value in table_foreign_keys: value = dict((k.lower(), v) for k, v in value.items()) name = value.get('constraint_name', '') if name not in foreign_keys: if 'delete_rule' not in value or value['delete_rule'] == 'RESTRICT': value['delete_rule'] = '' if 'update_rule' not in value or value['update_rule'] == 'RESTRICT': value['update_rule'] = '' foreign_keys[name] = { 'name': name, 'local': [], 'foreign': [], 'foreign_table': value['referenced_table_name'], 'on_delete': value['delete_rule'], 'on_update': value['update_rule'] } foreign_keys[name]['local'].append(value['column_name']) foreign_keys[name]['foreign'].append(value['referenced_column_name']) result = [] for constraint in foreign_keys.values(): result.append(ForeignKeyConstraint( constraint['local'], constraint['foreign_table'], constraint['foreign'], constraint['name'], { 'on_delete': constraint['on_delete'], 'on_update': constraint['on_update'] } )) return result PK!N[!orator/dbal/platforms/__init__.py# -*- coding: utf-8 -*- from .sqlite_platform import SQLitePlatform from .mysql_platform import MySQLPlatform from .mysql57_platform import MySQL57Platform from .postgres_platform import PostgresPlatform PK!*orator/dbal/platforms/keywords/__init__.py# -*- coding: utf-8 -*- PK!OV.orator/dbal/platforms/keywords/keyword_list.py# -*- coding: utf-8 -*- class KeywordList(object): KEYWORDS = [] def is_keyword(self, word): return word.upper() in self.KEYWORDS def get_name(self): raise NotImplementedError PK!Bl0orator/dbal/platforms/keywords/mysql_keywords.py# -*- coding: utf-8 -*- from .keyword_list import KeywordList class MySQLKeywords(KeywordList): KEYWORDS = [ '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', 'CONNECTION', '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', 'GOTO', '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', 'LABEL', 'LEADING', 'LEAVE', 'LEFT', 'LIKE', 'LIMIT', 'LINES', 'LOAD', 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCK', 'LONG', 'LONGBLOB', 'LONGTEXT', 'LOOP', 'LOW_PRIORITY', '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', 'RAID0', 'RANGE', 'READ', 'READS', 'REAL', 'REFERENCES', 'REGEXP', 'RELEASE', 'RENAME', 'REPEAT', 'REPLACE', 'REQUIRE', 'RESTRICT', 'RETURN', 'REVOKE', 'RIGHT', 'RLIKE', 'SCHEMA', 'SCHEMAS', 'SECOND_MICROSECOND', 'SELECT', 'SENSITIVE', 'SEPARATOR', 'SET', 'SHOW', 'SMALLINT', 'SONAME', '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' ] def get_name(self): return 'MySQL' PK!P5orator/dbal/platforms/keywords/postgresql_keywords.py# -*- coding: utf-8 -*- from .keyword_list import KeywordList class PostgreSQLKeywords(KeywordList): KEYWORDS = [ 'ALL', 'ANALYSE', 'ANALYZE', 'AND', 'ANY', 'AS', 'ASC', 'AUTHORIZATION', 'BETWEEN', 'BINARY', 'BOTH', 'CASE', 'CAST', 'CHECK', 'COLLATE', 'COLUMN', 'CONSTRAINT', 'CREATE', 'CURRENT_DATE', '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', 'SELECT', 'SESSION_USER', 'SIMILAR', 'SOME', 'TABLE', 'THEN', 'TO', 'TRAILING', 'TRUE', 'UNION', 'UNIQUE', 'USER', 'USING', 'VERBOSE', 'WHEN', 'WHERE' ] def get_name(self): return 'PostgreSQL' PK!o 1orator/dbal/platforms/keywords/sqlite_keywords.py# -*- coding: utf-8 -*- from .keyword_list import KeywordList class SQLiteKeywords(KeywordList): KEYWORDS = [ 'ABORT', 'ACTION', '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', 'EXISTS', 'EXPLAIN', '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', 'NO', 'NOT', 'NOTNULL', 'NULL', 'OF', 'OFFSET', 'ON', 'OR', 'ORDER', 'OUTER', 'PLAN', 'PRAGMA', 'PRIMARY', 'QUERY', 'RAISE', 'REFERENCES', 'REGEXP', 'REINDEX', 'RELEASE', 'RENAME', 'REPLACE', 'RESTRICT', 'RIGHT', 'ROLLBACK', 'ROW', 'SAVEPOINT', 'SELECT', 'SET', 'TABLE', 'TEMP', 'TEMPORARY', 'THEN', 'TO', 'TRANSACTION', 'TRIGGER', 'UNION', 'UNIQUE', 'UPDATE', 'USING', 'VACUUM', 'VALUES', 'VIEW', 'VIRTUAL', 'WHEN', 'WHERE' ] def get_name(self): return 'SQLite' PK!͓)orator/dbal/platforms/mysql57_platform.py# -*- coding: utf-8 -*- from .mysql_platform import MySQLPlatform class MySQL57Platform(MySQLPlatform): INTERNAL_TYPE_MAPPING = { 'tinyint': 'boolean', 'smallint': 'smallint', 'mediumint': 'integer', 'int': 'integer', 'integer': 'integer', 'bigint': 'bigint', 'int8': 'bigint', 'bool': 'boolean', 'boolean': 'boolean', 'tinytext': 'text', 'mediumtext': 'text', 'longtext': 'text', 'text': 'text', 'varchar': 'string', 'string': 'string', 'char': 'string', 'date': 'date', 'datetime': 'datetime', 'timestamp': 'datetime', 'time': 'time', 'float': 'float', 'double': 'float', 'real': 'float', 'decimal': 'decimal', 'numeric': 'decimal', 'year': 'date', 'longblob': 'blob', 'blob': 'blob', 'mediumblob': 'blob', 'tinyblob': 'blob', 'binary': 'binary', 'varbinary': 'binary', 'set': 'simple_array', 'enum': 'enum', 'json': 'json', } def get_json_type_declaration_sql(self, column): return 'JSON' def has_native_json_type(self): return True PK!x_$_$'orator/dbal/platforms/mysql_platform.py# -*- coding: utf-8 -*- from .platform import Platform from .keywords.mysql_keywords import MySQLKeywords from ..identifier import Identifier class MySQLPlatform(Platform): LENGTH_LIMIT_TINYTEXT = 255 LENGTH_LIMIT_TEXT = 65535 LENGTH_LIMIT_MEDIUMTEXT = 16777215 LENGTH_LIMIT_TINYBLOB = 255 LENGTH_LIMIT_BLOB = 65535 LENGTH_LIMIT_MEDIUMBLOB = 16777215 INTERNAL_TYPE_MAPPING = { 'tinyint': 'boolean', 'smallint': 'smallint', 'mediumint': 'integer', 'int': 'integer', 'integer': 'integer', 'bigint': 'bigint', 'int8': 'bigint', 'bool': 'boolean', 'boolean': 'boolean', 'tinytext': 'text', 'mediumtext': 'text', 'longtext': 'text', 'text': 'text', 'varchar': 'string', 'string': 'string', 'char': 'string', 'date': 'date', 'datetime': 'datetime', 'timestamp': 'datetime', 'time': 'time', 'float': 'float', 'double': 'float', 'real': 'float', 'decimal': 'decimal', 'numeric': 'decimal', 'year': 'date', 'longblob': 'blob', 'blob': 'blob', 'mediumblob': 'blob', 'tinyblob': 'blob', 'binary': 'binary', 'varbinary': 'binary', 'set': 'simple_array', 'enum': 'enum', } def get_list_table_columns_sql(self, table, database=None): if database: database = "'%s'" % database else: database = 'DATABASE()' return 'SELECT COLUMN_NAME AS field, COLUMN_TYPE AS type, IS_NULLABLE AS `null`, ' \ 'COLUMN_KEY AS `key`, COLUMN_DEFAULT AS `default`, EXTRA AS extra, COLUMN_COMMENT AS comment, ' \ 'CHARACTER_SET_NAME AS character_set, COLLATION_NAME AS collation ' \ 'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = \'%s\''\ % (database, table) def get_list_table_indexes_sql(self, table, current_database=None): sql = """ SELECT TABLE_NAME AS `Table`, NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, SEQ_IN_INDEX AS Seq_in_index, COLUMN_NAME AS Column_Name, COLLATION AS Collation, CARDINALITY AS Cardinality, SUB_PART AS Sub_Part, PACKED AS Packed, NULLABLE AS `Null`, INDEX_TYPE AS Index_Type, COMMENT AS Comment FROM information_schema.STATISTICS WHERE TABLE_NAME = '%s' """ if current_database: sql += ' AND TABLE_SCHEMA = \'%s\'' % current_database return sql % table def get_list_table_foreign_keys_sql(self, table, database=None): sql = ("SELECT DISTINCT k.`CONSTRAINT_NAME`, k.`COLUMN_NAME`, k.`REFERENCED_TABLE_NAME`, " "k.`REFERENCED_COLUMN_NAME` /*!50116 , c.update_rule, c.delete_rule */ " "FROM information_schema.key_column_usage k /*!50116 " "INNER JOIN information_schema.referential_constraints c ON " " c.constraint_name = k.constraint_name AND " " c.table_name = '%s' */ WHERE k.table_name = '%s'" % (table, table)) if database: sql += " AND k.table_schema = '%s' /*!50116 AND c.constraint_schema = '%s' */"\ % (database, database) sql += " AND k.`REFERENCED_COLUMN_NAME` IS NOT NULL" return sql def get_alter_table_sql(self, diff): """ Get the ALTER TABLE SQL statement :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: list """ column_sql = [] query_parts = [] if diff.new_name is not False: query_parts.append('RENAME TO %s' % diff.get_new_name().get_quoted_name(self)) # Added columns? # Removed columns? for column_diff in diff.changed_columns.values(): column = column_diff.column column_dict = column.to_dict() # Don't propagate default value changes for unsupported column types. if column_diff.has_changed('default') \ and len(column_diff.changed_properties) == 1 \ and (column_dict['type'] == 'text' or column_dict['type'] == 'blob'): continue query_parts.append('CHANGE %s %s' % (column_diff.get_old_column_name().get_quoted_name(self), self.get_column_declaration_sql(column.get_quoted_name(self), column_dict))) for old_column_name, column in diff.renamed_columns.items(): column_dict = column.to_dict() old_column_name = Identifier(old_column_name) query_parts.append('CHANGE %s %s' % (self.quote(old_column_name.get_quoted_name(self)), self.get_column_declaration_sql( self.quote(column.get_quoted_name(self)), column_dict))) sql = [] if len(query_parts) > 0: sql.append('ALTER TABLE %s %s' % (diff.get_name(self).get_quoted_name(self), ', '.join(query_parts))) return sql def convert_booleans(self, item): if isinstance(item, list): for i, value in enumerate(item): if isinstance(value, bool): item[i] = str(value).lower() elif isinstance(item, bool): item = str(item).lower() return item def get_boolean_type_declaration_sql(self, column): return 'TINYINT(1)' def get_integer_type_declaration_sql(self, column): return 'INT ' + self._get_common_integer_type_declaration_sql(column) def get_bigint_type_declaration_sql(self, column): return 'BIGINT ' + self._get_common_integer_type_declaration_sql(column) def get_smallint_type_declaration_sql(self, column): return 'SMALLINT ' + self._get_common_integer_type_declaration_sql(column) def get_guid_type_declaration_sql(self, column): return 'UUID' def get_datetime_type_declaration_sql(self, column): if 'version' in column and column['version'] == True: return 'TIMESTAMP' return 'DATETIME' def get_date_type_declaration_sql(self, column): return 'DATE' def get_time_type_declaration_sql(self, column): return 'TIME' def get_varchar_type_declaration_sql_snippet(self, length, fixed): if fixed: return 'CHAR(%s)' % length if length else 'CHAR(255)' else: return 'VARCHAR(%s)' % length if length else 'VARCHAR(255)' def get_binary_type_declaration_sql_snippet(self, length, fixed): if fixed: return 'BINARY(%s)' % (length or 255) else: return 'VARBINARY(%s)' % (length or 255) def get_text_type_declaration_sql(self, column): length = column.get('length') if length: if length <= self.LENGTH_LIMIT_TINYTEXT: return 'TINYTEXT' if length <= self.LENGTH_LIMIT_TEXT: return 'TEXT' if length <= self.LENGTH_LIMIT_MEDIUMTEXT: return 'MEDIUMTEXT' return 'LONGTEXT' def get_blob_type_declaration_sql(self, column): length = column.get('length') if length: if length <= self.LENGTH_LIMIT_TINYBLOB: return 'TINYBLOB' if length <= self.LENGTH_LIMIT_BLOB: return 'BLOB' if length <= self.LENGTH_LIMIT_MEDIUMBLOB: return 'MEDIUMBLOB' return 'LONGBLOB' def get_clob_type_declaration_sql(self, column): length = column.get('length') if length: if length <= self.LENGTH_LIMIT_TINYTEXT: return 'TINYTEXT' if length <= self.LENGTH_LIMIT_TEXT: return 'TEXT' if length <= self.LENGTH_LIMIT_MEDIUMTEXT: return 'MEDIUMTEXT' return 'LONGTEXT' def get_decimal_type_declaration_sql(self, column): decl = super(MySQLPlatform, self).get_decimal_type_declaration_sql(column) return decl + self.get_unsigned_declaration(column) def get_unsigned_declaration(self, column): if column.get('unsigned'): return ' UNSIGNED' return '' def _get_common_integer_type_declaration_sql(self, column): autoinc = '' if column.get('autoincrement'): autoinc = ' AUTO_INCREMENT' return self.get_unsigned_declaration(column) + autoinc def get_float_type_declaration_sql(self, column): return 'DOUBLE PRECISION' + self.get_unsigned_declaration(column) def get_enum_type_declaration_sql(self, column): return 'ENUM{}'.format(column['extra']['definition']) def supports_foreign_key_constraints(self): return True def supports_column_collation(self): return False def quote(self, name): return '`%s`' % name.replace('`', '``') def _get_reserved_keywords_class(self): return MySQLKeywords def get_identifier_quote_character(self): return '`' PK!RR!orator/dbal/platforms/platform.py# -*- coding: utf-8 -*- from collections import OrderedDict from ..index import Index from ..table import Table from ..identifier import Identifier from ..exceptions import DBALException from ...utils import basestring class Platform(object): _keywords = None CREATE_INDEXES = 1 CREATE_FOREIGNKEYS = 2 INTERNAL_TYPE_MAPPING = {} def __init__(self, version=None): self._version = None def get_default_value_declaration_sql(self, field): default = '' if not field.get('notnull'): default = ' DEFAULT NULL' if 'default' in field and field['default'] is not None: default = ' DEFAULT \'%s\'' % field['default'] if 'type' in field: type = field['type'] if type in ['integer', 'bigint', 'smallint']: default = ' DEFAULT %s' % field['default'] elif type in ['datetime', 'datetimetz'] \ and field['default'] in [self.get_current_timestamp_sql(), 'NOW', 'now']: default = ' DEFAULT %s' % self.get_current_timestamp_sql() elif type in ['time'] \ and field['default'] in [self.get_current_time_sql(), 'NOW', 'now']: default = ' DEFAULT %s' % self.get_current_time_sql() elif type in ['date'] \ and field['default'] in [self.get_current_date_sql(), 'NOW', 'now']: default = ' DEFAULT %s' % self.get_current_date_sql() elif type in ['boolean']: default = ' DEFAULT \'%s\'' % self.convert_booleans(field['default']) return default def convert_booleans(self, item): if isinstance(item, list): for i, value in enumerate(item): if isinstance(value, bool): item[i] = int(value) elif isinstance(item, bool): item = int(item) return item def get_check_declaration_sql(self, definition): """ Obtains DBMS specific SQL code portion needed to set a CHECK constraint declaration to be used in statements like CREATE TABLE. :param definition: The check definition :type definition: dict :return: DBMS specific SQL code portion needed to set a CHECK constraint. :rtype: str """ constraints = [] for field, def_ in definition.items(): if isinstance(def_, basestring): constraints.append('CHECK (%s)' % def_) else: if 'min' in def_: constraints.append('CHECK (%s >= %s)' % (field, def_['min'])) if 'max' in def_: constraints.append('CHECK (%s <= %s)' % (field, def_['max'])) return ', '.join(constraints) def get_unique_constraint_declaration_sql(self, name, index): """ Obtains DBMS specific SQL code portion needed to set a unique constraint declaration to be used in statements like CREATE TABLE. :param name: The name of the unique constraint. :type name: str :param index: The index definition :type index: Index :return: DBMS specific SQL code portion needed to set a constraint. :rtype: str """ columns = index.get_quoted_columns(self) name = Identifier(name) if not columns: raise DBALException('Incomplete definition. "columns" required.') return 'CONSTRAINT %s UNIQUE (%s)%s'\ % (name.get_quoted_name(self), self.get_index_field_declaration_list_sql(columns), self.get_partial_index_sql(index)) def get_index_declaration_sql(self, name, index): """ Obtains DBMS specific SQL code portion needed to set an index declaration to be used in statements like CREATE TABLE. :param name: The name of the index. :type name: str :param index: The index definition :type index: Index :return: DBMS specific SQL code portion needed to set an index. :rtype: str """ columns = index.get_quoted_columns(self) name = Identifier(name) if not columns: raise DBALException('Incomplete definition. "columns" required.') return '%sINDEX %s (%s)%s'\ % (self.get_create_index_sql_flags(index), name.get_quoted_name(self), self.get_index_field_declaration_list_sql(columns), self.get_partial_index_sql(index)) def get_foreign_key_declaration_sql(self, foreign_key): """ Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint of a field declaration to be used in statements like CREATE TABLE. :param foreign_key: The foreign key :type foreign_key: ForeignKeyConstraint :rtype: str """ sql = self.get_foreign_key_base_declaration_sql(foreign_key) sql += self.get_advanced_foreign_key_options_sql(foreign_key) return sql def get_advanced_foreign_key_options_sql(self, foreign_key): """ Returns the FOREIGN KEY query section dealing with non-standard options as MATCH, INITIALLY DEFERRED, ON UPDATE, ... :param foreign_key: The foreign key :type foreign_key: ForeignKeyConstraint :rtype: str """ query = '' if self.supports_foreign_key_on_update() and foreign_key.has_option('on_update'): query += ' ON UPDATE %s' % self.get_foreign_key_referential_action_sql(foreign_key.get_option('on_update')) if foreign_key.has_option('on_delete'): query += ' ON DELETE %s' % self.get_foreign_key_referential_action_sql(foreign_key.get_option('on_delete')) return query def get_foreign_key_referential_action_sql(self, action): """ Returns the given referential action in uppercase if valid, otherwise throws an exception. :param action: The action :type action: str :rtype: str """ action = action.upper() if action not in ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']: raise DBALException('Invalid foreign key action: %s' % action) return action def get_foreign_key_base_declaration_sql(self, foreign_key): """ Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint of a field declaration to be used in statements like CREATE TABLE. :param foreign_key: The foreign key :type foreign_key: ForeignKeyConstraint :rtype: str """ sql = '' if foreign_key.get_name(): sql += 'CONSTRAINT %s ' % foreign_key.get_quoted_name(self) sql += 'FOREIGN KEY (' if not foreign_key.get_local_columns(): raise DBALException('Incomplete definition. "local" required.') if not foreign_key.get_foreign_columns(): raise DBALException('Incomplete definition. "foreign" required.') if not foreign_key.get_foreign_table_name(): raise DBALException('Incomplete definition. "foreign_table" required.') sql += '%s) REFERENCES %s (%s)'\ % (', '.join(foreign_key.get_quoted_local_columns(self)), foreign_key.get_quoted_foreign_table_name(self), ', '.join(foreign_key.get_quoted_foreign_columns(self))) return sql def get_current_date_sql(self): return 'CURRENT_DATE' def get_current_time_sql(self): return 'CURRENT_TIME' def get_current_timestamp_sql(self): return 'CURRENT_TIMESTAMP' def get_sql_type_declaration(self, column): internal_type = column['type'] return getattr(self, 'get_%s_type_declaration_sql' % internal_type)(column) def get_column_declaration_list_sql(self, fields): """ Gets declaration of a number of fields in bulk. """ query_fields = [] for name, field in fields.items(): query_fields.append(self.get_column_declaration_sql(name, field)) return ', '.join(query_fields) def get_column_declaration_sql(self, name, field): if 'column_definition' in field: column_def = self.get_custom_type_declaration_sql(field) else: default = self.get_default_value_declaration_sql(field) charset = field.get('charset', '') if charset: charset = ' ' + self.get_column_charset_declaration_sql(charset) collation = field.get('collation', '') if charset: charset = ' ' + self.get_column_collation_declaration_sql(charset) notnull = field.get('notnull', '') if notnull: notnull = ' NOT NULL' else: notnull = '' unique = field.get('unique', '') if unique: unique = ' ' + self.get_unique_field_declaration_sql() else: unique = '' check = field.get('check', '') type_decl = self.get_sql_type_declaration(field) column_def = type_decl + charset + default + notnull + unique + check + collation return name + ' ' + column_def def get_custom_type_declaration_sql(self, column_def): return column_def['column_definition'] def get_column_charset_declaration_sql(self, charset): return '' def get_column_collation_declaration_sql(self, collation): if self.supports_column_collation(): return 'COLLATE %s' % collation return '' def supports_column_collation(self): return False def get_unique_field_declaration_sql(self): return 'UNIQUE' def get_string_type_declaration_sql(self, column): if 'length' not in column: column['length'] = self.get_varchar_default_length() fixed = column.get('fixed', False) if column['length'] > self.get_varchar_max_length(): return self.get_clob_type_declaration_sql(column) return self.get_varchar_type_declaration_sql_snippet(column['length'], fixed) def get_binary_type_declaration_sql(self, column): if 'length' not in column: column['length'] = self.get_binary_default_length() fixed = column.get('fixed', False) if column['length'] > self.get_binary_max_length(): return self.get_blob_type_declaration_sql(column) return self.get_binary_type_declaration_sql_snippet(column['length'], fixed) def get_varchar_type_declaration_sql_snippet(self, length, fixed): raise NotImplementedError('VARCHARS not supported by Platform') def get_binary_type_declaration_sql_snippet(self, length, fixed): raise NotImplementedError('BINARY/VARBINARY not supported by Platform') def get_decimal_type_declaration_sql(self, column): if 'precision' not in column or not column['precision']: column['precision'] = 10 if 'scale' not in column or not column['scale']: column['precision'] = 0 return 'NUMERIC(%s, %s)' % (column['precision'], column['scale']) def get_json_type_declaration_sql(self, column): return self.get_clob_type_declaration_sql(column) def get_clob_type_declaration_sql(self, column): raise NotImplementedError() def get_text_type_declaration_sql(self, column): return self.get_clob_type_declaration_sql(column) def get_blob_type_declaration_sql(self, column): raise NotImplementedError() def get_varchar_default_length(self): return 255 def get_varchar_max_length(self): return 4000 def get_binary_default_length(self): return 255 def get_binary_max_length(self): return 4000 def get_column_options(self): return [] def get_type_mapping(self, db_type): return self.INTERNAL_TYPE_MAPPING[db_type] def get_reserved_keywords_list(self): if self._keywords: return self._keywords klass = self._get_reserved_keywords_class() keywords = klass() self._keywords = keywords return keywords def _get_reserved_keywords_class(self): raise NotImplementedError def get_index_field_declaration_list_sql(self, fields): """ Obtains DBMS specific SQL code portion needed to set an index declaration to be used in statements like CREATE TABLE. :param fields: The columns :type fields: list :rtype: sql """ ret = [] for field in fields: ret.append(field) return ', '.join(ret) def get_create_index_sql(self, index, table): """ Returns the SQL to create an index on a table on this platform. :param index: The index :type index: Index :param table: The table :type table: Table or str :rtype: str """ if isinstance(table, Table): table = table.get_quoted_name(self) name = index.get_quoted_name(self) columns = index.get_quoted_columns(self) if not columns: raise DBALException('Incomplete definition. "columns" required.') if index.is_primary(): return self.get_create_primary_key_sql(index, table) query = 'CREATE %sINDEX %s ON %s' % (self.get_create_index_sql_flags(index), name, table) query += ' (%s)%s' % (self.get_index_field_declaration_list_sql(columns), self.get_partial_index_sql(index)) return query def get_partial_index_sql(self, index): """ Adds condition for partial index. :param index: The index :type index: Index :rtype: str """ if self.supports_partial_indexes() and index.has_option('where'): return ' WHERE %s' % index.get_option('where') return '' def get_create_index_sql_flags(self, index): """ Adds additional flags for index generation. :param index: The index :type index: Index :rtype: str """ if index.is_unique(): return 'UNIQUE ' return '' def get_create_primary_key_sql(self, index, table): """ Returns the SQL to create an unnamed primary key constraint. :param index: The index :type index: Index :param table: The table :type table: Table or str :rtype: str """ return 'ALTER TABLE %s ADD PRIMARY KEY (%s)'\ % (table, self.get_index_field_declaration_list_sql(index.get_quoted_columns(self))) def get_create_foreign_key_sql(self, foreign_key, table): """ Returns the SQL to create a new foreign key. :rtype: sql """ if isinstance(table, Table): table = table.get_quoted_name(self) query = 'ALTER TABLE %s ADD %s' % (table, self.get_foreign_key_declaration_sql(foreign_key)) return query def get_drop_table_sql(self, table): """ Returns the SQL snippet to drop an existing table. :param table: The table :type table: Table or str :rtype: str """ if isinstance(table, Table): table = table.get_quoted_name(self) return 'DROP TABLE %s' % table def get_drop_index_sql(self, index, table=None): """ Returns the SQL to drop an index from a table. :param index: The index :type index: Index or str :param table: The table :type table: Table or str or None :rtype: str """ if isinstance(index, Index): index = index.get_quoted_name(self) return 'DROP INDEX %s' % index def get_create_table_sql(self, table, create_flags=CREATE_INDEXES): """ Returns the SQL statement(s) to create a table with the specified name, columns and constraints on this platform. :param table: The table :type table: Table :type create_flags: int :rtype: str """ table_name = table.get_quoted_name(self) options = dict((k, v) for k, v in table.get_options().items()) options['unique_constraints'] = OrderedDict() options['indexes'] = OrderedDict() options['primary'] = [] if create_flags & self.CREATE_INDEXES > 0: for index in table.get_indexes().values(): if index.is_primary(): options['primary'] = index.get_quoted_columns(self) options['primary_index'] = index else: options['indexes'][index.get_quoted_name(self)] = index columns = OrderedDict() for column in table.get_columns().values(): column_data = column.to_dict() column_data['name'] = column.get_quoted_name(self) if column.has_platform_option('version'): column_data['version'] = column.get_platform_option('version') else: column_data['version'] = False # column_data['comment'] = self.get_column_comment(column) if column_data['type'] == 'string' and column_data['length'] is None: column_data['length'] = 255 if column.get_name() in options['primary']: column_data['primary'] = True columns[column_data['name']] = column_data if create_flags & self.CREATE_FOREIGNKEYS > 0: options['foreign_keys'] = [] for fk in table.get_foreign_keys().values(): options['foreign_keys'].append(fk) sql = self._get_create_table_sql(table_name, columns, options) # Comments? return sql def _get_create_table_sql(self, table_name, columns, options=None): """ Returns the SQL used to create a table. :param table_name: The name of the table to create :type table_name: str :param columns: The table columns :type columns: dict :param options: The options :type options: dict :rtype: str """ options = options or {} column_list_sql = self.get_column_declaration_list_sql(columns) if options.get('unique_constraints'): for name, definition in options['unique_constraints'].items(): column_list_sql += ', %s' % self.get_unique_constraint_declaration_sql(name, definition) if options.get('primary'): column_list_sql += ', PRIMARY KEY(%s)' % ', '.join(options['primary']) if options.get('indexes'): for index, definition in options['indexes']: column_list_sql += ', %s' % self.get_index_declaration_sql(index, definition) query = 'CREATE TABLE %s (%s' % (table_name, column_list_sql) check = self.get_check_declaration_sql(columns) if check: query += ', %s' % check query += ')' sql = [query] if options.get('foreign_keys'): for definition in options['foreign_keys']: sql.append(self.get_create_foreign_key_sql(definition, table_name)) return sql def quote_identifier(self, string): """ Quotes a string so that it can be safely used as a table or column name, even if it is a reserved word of the platform. This also detects identifier chains separated by dot and quotes them independently. :param string: The identifier name to be quoted. :type string: str :return: The quoted identifier string. :rtype: str """ if '.' in string: parts = list(map(self.quote_single_identifier, string.split('.'))) return '.'.join(parts) return self.quote_single_identifier(string) def quote_single_identifier(self, string): """ Quotes a single identifier (no dot chain separation). :param string: The identifier name to be quoted. :type string: str :return: The quoted identifier string. :rtype: str """ c = self.get_identifier_quote_character() return '%s%s%s' % (c, string.replace(c, c+c), c) def get_identifier_quote_character(self): return '"' def supports_indexes(self): return True def supports_partial_indexes(self): return False def supports_alter_table(self): return True def supports_transactions(self): return True def supports_primary_constraints(self): return True def supports_foreign_key_constraints(self): return True def supports_foreign_key_on_update(self): return self.supports_foreign_key_constraints() def has_native_json_type(self): return False PK!BkL0L0*orator/dbal/platforms/postgres_platform.py# -*- coding: utf-8 -*- from .platform import Platform from .keywords.postgresql_keywords import PostgreSQLKeywords from ..table import Table from ..column import Column from ..identifier import Identifier class PostgresPlatform(Platform): INTERNAL_TYPE_MAPPING = { 'smallint': 'smallint', 'int2': 'smallint', 'serial': 'integer', 'serial4': 'integer', 'int': 'integer', 'int4': 'integer', 'integer': 'integer', 'bigserial': 'bigint', 'serial8': 'bigint', 'bigint': 'bigint', 'int8': 'bigint', 'bool': 'boolean', 'boolean': 'boolean', 'text': 'text', 'tsvector': 'text', 'varchar': 'string', 'interval': 'string', '_varchar': 'string', 'char': 'string', 'bpchar': 'string', 'inet': 'string', 'date': 'date', 'datetime': 'datetime', 'timestamp': 'datetime', 'timestamptz': 'datetimez', 'time': 'time', 'timetz': 'time', 'float': 'float', 'float4': 'float', 'float8': 'float', 'double': 'float', 'double precision': 'float', 'real': 'float', 'decimal': 'decimal', 'money': 'decimal', 'numeric': 'decimal', 'year': 'date', 'uuid': 'guid', 'bytea': 'blob', 'json': 'json' } def get_list_table_columns_sql(self, table): sql = """SELECT a.attnum, quote_ident(a.attname) AS field, t.typname AS type, format_type(a.atttypid, a.atttypmod) AS complete_type, (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, a.attnotnull AS isnotnull, (SELECT 't' FROM pg_index WHERE c.oid = pg_index.indrelid AND pg_index.indkey[0] = a.attnum AND pg_index.indisprimary = 't' ) AS pri, (SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE c.oid = pg_attrdef.adrelid AND pg_attrdef.adnum=a.attnum ) AS default, (SELECT pg_description.description FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid ) AS comment FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n WHERE %s AND a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid AND n.oid = c.relnamespace ORDER BY a.attnum""" % self.get_table_where_clause(table) return sql def get_list_table_indexes_sql(self, table): sql = """ SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary, pg_index.indkey, pg_index.indrelid, pg_get_expr(indpred, indrelid) AS where FROM pg_class, pg_index WHERE oid IN ( SELECT indexrelid FROM pg_index si, pg_class sc, pg_namespace sn WHERE %s AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid ) AND pg_index.indexrelid = oid""" sql = sql % self.get_table_where_clause(table, 'sc', 'sn') return sql def get_list_table_foreign_keys_sql(self, table): return 'SELECT quote_ident(r.conname) as conname, ' \ 'pg_catalog.pg_get_constraintdef(r.oid, true) AS condef ' \ 'FROM pg_catalog.pg_constraint r ' \ 'WHERE r.conrelid = ' \ '(' \ 'SELECT c.oid ' \ 'FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n ' \ 'WHERE ' + self.get_table_where_clause(table) + ' AND n.oid = c.relnamespace' \ ')' \ ' AND r.contype = \'f\'' def get_table_where_clause(self, table, class_alias='c', namespace_alias='n'): where_clause = namespace_alias + '.nspname NOT IN (\'pg_catalog\', \'information_schema\', \'pg_toast\') AND ' if table.find('.') >= 0: split = table.split('.') schema, table = split[0], split[1] schema = "'%s'" % schema else: schema = 'ANY(string_to_array((select replace(replace(setting, \'"$user"\', user), \' \', \'\')' \ ' from pg_catalog.pg_settings where name = \'search_path\'),\',\'))' where_clause += '%s.relname = \'%s\' AND %s.nspname = %s' % (class_alias, table, namespace_alias, schema) return where_clause def get_advanced_foreign_key_options_sql(self, foreign_key): query = '' if foreign_key.has_option('match'): query += ' MATCH %s' % foreign_key.get_option('match') query += super(PostgresPlatform, self).get_advanced_foreign_key_options_sql(foreign_key) deferrable = foreign_key.has_option('deferrable') and foreign_key.get_option('deferrable') is not False if deferrable: query += ' DEFERRABLE' else: query += ' NOT DEFERRABLE' query += ' INITIALLY' deferred = foreign_key.has_option('deferred') and foreign_key.get_option('deferred') is not False if deferred: query += ' DEFERRED' else: query += ' IMMEDIATE' return query def get_alter_table_sql(self, diff): """ Get the ALTER TABLE SQL statement :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: list """ sql = [] for column_diff in diff.changed_columns.values(): if self.is_unchanged_binary_column(column_diff): continue old_column_name = column_diff.get_old_column_name().get_quoted_name(self) column = column_diff.column if any([column_diff.has_changed('type'), column_diff.has_changed('precision'), column_diff.has_changed('scale'), column_diff.has_changed('fixed')]): query = 'ALTER ' + old_column_name + ' TYPE ' + self.get_sql_type_declaration(column.to_dict()) sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' + query) if column_diff.has_changed('default') or column_diff.has_changed('type'): if column.get_default() is None: default_clause = ' DROP DEFAULT' else: default_clause = ' SET' + self.get_default_value_declaration_sql(column.to_dict()) query = 'ALTER ' + old_column_name + default_clause sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' + query) if column_diff.has_changed('notnull'): op = 'DROP' if column.get_notnull(): op = 'SET' query = 'ALTER ' + old_column_name + ' ' + op + ' NOT NULL' sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' + query) if column_diff.has_changed('autoincrement'): if column.get_autoincrement(): seq_name = self.get_identity_sequence_name(diff.name, old_column_name) sql.append('CREATE SEQUENCE ' + seq_name) sql.append('SELECT setval(\'' + seq_name + '\', ' '(SELECT MAX(' + old_column_name + ') FROM ' + diff.name + '))') query = 'ALTER ' + old_column_name + ' SET DEFAULT nextval(\'' + seq_name + '\')' sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' + query) else: query = 'ALTER ' + old_column_name + ' DROP DEFAULT' sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' + query) if column_diff.has_changed('length'): query = 'ALTER ' + old_column_name + ' TYPE ' + self.get_sql_type_declaration(column.to_dict()) sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' + query) for old_column_name, column in diff.renamed_columns.items(): sql.append('ALTER TABLE ' + diff.get_name(self).get_quoted_name(self) + ' ' 'RENAME COLUMN ' + Identifier(old_column_name).get_quoted_name(self) + ' TO ' + column.get_quoted_name(self)) return sql def is_unchanged_binary_column(self, column_diff): column_type = column_diff.column.get_type() if column_type not in ['blob', 'binary']: return False if isinstance(column_diff.from_column, Column): from_column = column_diff.from_column else: from_column = None if from_column: from_column_type = self.INTERNAL_TYPE_MAPPING[from_column.get_type()] if from_column_type in ['blob', 'binary']: return False return len([x for x in column_diff.changed_properties if x not in ['type', 'length', 'fixed']]) == 0 if column_diff.has_changed('type'): return False return len([x for x in column_diff.changed_properties if x not in ['length', 'fixed']]) == 0 def convert_booleans(self, item): if isinstance(item, list): for i, value in enumerate(item): if isinstance(value, bool): item[i] = str(value).lower() elif isinstance(item, bool): item = str(item).lower() return item def get_boolean_type_declaration_sql(self, column): return 'BOOLEAN' def get_integer_type_declaration_sql(self, column): if column.get('autoincrement'): return 'SERIAL' return 'INT' def get_bigint_type_declaration_sql(self, column): if column.get('autoincrement'): return 'BIGSERIAL' return 'BIGINT' def get_smallint_type_declaration_sql(self, column): return 'SMALLINT' def get_guid_type_declaration_sql(self, column): return 'UUID' def get_datetime_type_declaration_sql(self, column): return 'TIMESTAMP(0) WITHOUT TIME ZONE' def get_datetimetz_type_declaration_sql(self, column): return 'TIMESTAMP(0) WITH TIME ZONE' def get_date_type_declaration_sql(self, column): return 'DATE' def get_time_type_declaration_sql(self, column): return 'TIME(0) WITHOUT TIME ZONE' def get_string_type_declaration_sql(self, column): length = column.get('length', '255') fixed = column.get('fixed') if fixed: return 'CHAR(%s)' % length else: return 'VARCHAR(%s)' % length def get_binary_type_declaration_sql(self, column): return 'BYTEA' def get_blob_type_declaration_sql(self, column): return 'BYTEA' def get_clob_type_declaration_sql(self, column): return 'TEXT' def get_text_type_declaration_sql(self, column): return 'TEXT' def get_json_type_declaration_sql(self, column): return 'JSON' def get_decimal_type_declaration_sql(self, column): if 'precision' not in column or not column['precision']: column['precision'] = 10 if 'scale' not in column or not column['scale']: column['precision'] = 0 return 'DECIMAL(%s, %s)' % (column['precision'], column['scale']) def get_float_type_declaration_sql(self, column): return 'DOUBLE PRECISION' def supports_foreign_key_constraints(self): return True def has_native_json_type(self): return True def _get_reserved_keywords_class(self): return PostgreSQLKeywords PK!U^|NN(orator/dbal/platforms/sqlite_platform.py# -*- coding: utf-8 -*- from collections import OrderedDict from .platform import Platform from .keywords.sqlite_keywords import SQLiteKeywords from ..table import Table from ..index import Index from ..column import Column from ..identifier import Identifier from ..foreign_key_constraint import ForeignKeyConstraint from ..exceptions import DBALException class SQLitePlatform(Platform): INTERNAL_TYPE_MAPPING = { 'boolean': 'boolean', 'tinyint': 'boolean', 'smallint': 'smallint', 'mediumint': 'integer', 'int': 'integer', 'integer': 'integer', 'serial': 'integer', 'bigint': 'bigint', 'bigserial': 'bigint', 'clob': 'text', 'tinytext': 'text', 'mediumtext': 'text', 'longtext': 'text', 'text': 'text', 'varchar': 'string', 'longvarchar': 'string', 'varchar2': 'string', 'nvarchar': 'string', 'image': 'string', 'ntext': 'string', 'char': 'string', 'date': 'date', 'datetime': 'datetime', 'timestamp': 'datetime', 'time': 'time', 'float': 'float', 'double': 'float', 'double precision': 'float', 'real': 'float', 'decimal': 'decimal', 'numeric': 'decimal', 'blob': 'blob', } def get_list_table_columns_sql(self, table): table = table.replace('.', '__') return 'PRAGMA table_info(\'%s\')' % table def get_list_table_indexes_sql(self, table): table = table.replace('.', '__') return 'PRAGMA index_list(\'%s\')' % table def get_list_table_foreign_keys_sql(self, table): table = table.replace('.', '__') return 'PRAGMA foreign_key_list(\'%s\')' % table def get_pre_alter_table_index_foreign_key_sql(self, diff): """ :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: list """ if not isinstance(diff.from_table, Table): raise DBALException('Sqlite platform requires for alter table the table' 'diff with reference to original table schema') sql = [] for index in diff.from_table.get_indexes().values(): if not index.is_primary(): sql.append(self.get_drop_index_sql(index, diff.name)) return sql def get_post_alter_table_index_foreign_key_sql(self, diff): """ :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: list """ if not isinstance(diff.from_table, Table): raise DBALException('Sqlite platform requires for alter table the table' 'diff with reference to original table schema') sql = [] if diff.new_name: table_name = diff.get_new_name() else: table_name = diff.get_name(self) for index in self._get_indexes_in_altered_table(diff).values(): if index.is_primary(): continue sql.append(self.get_create_index_sql(index, table_name.get_quoted_name(self))) return sql def get_create_table_sql(self, table, create_flags=None): if not create_flags: create_flags = self.CREATE_INDEXES | self.CREATE_FOREIGNKEYS return super(SQLitePlatform, self).get_create_table_sql(table, create_flags) def _get_create_table_sql(self, table_name, columns, options=None): table_name = table_name.replace('.', '__') query_fields = self.get_column_declaration_list_sql(columns) if options.get('unique_constraints'): for name, definition in options['unique_constraints'].items(): query_fields += ', %s' % self.get_unique_constraint_declaration_sql(name, definition) if options.get('primary'): key_columns = options['primary'] query_fields += ', PRIMARY KEY(%s)' % ', '.join(key_columns) if options.get('foreign_keys'): for foreign_key in options['foreign_keys']: query_fields += ', %s' % self.get_foreign_key_declaration_sql(foreign_key) query = [ 'CREATE TABLE %s (%s)' % (table_name, query_fields) ] if options.get('alter'): return query if options.get('indexes'): for index_def in options['indexes'].values(): query.append(self.get_create_index_sql(index_def, table_name)) if options.get('unique'): for index_def in options['unique'].values(): query.append(self.get_create_index_sql(index_def, table_name)) return query def get_foreign_key_declaration_sql(self, foreign_key): return super(SQLitePlatform, self).get_foreign_key_declaration_sql( ForeignKeyConstraint( foreign_key.get_quoted_local_columns(self), foreign_key.get_quoted_foreign_table_name(self).replace('.', '__'), foreign_key.get_quoted_foreign_columns(self), foreign_key.get_name(), foreign_key.get_options() ) ) def get_advanced_foreign_key_options_sql(self, foreign_key): query = super(SQLitePlatform, self).get_advanced_foreign_key_options_sql(foreign_key) deferrable = foreign_key.has_option('deferrable') and foreign_key.get_option('deferrable') is not False if deferrable: query += ' DEFERRABLE' else: query += ' NOT DEFERRABLE' query += ' INITIALLY' deferred = foreign_key.has_option('deferred') and foreign_key.get_option('deferred') is not False if deferred: query += ' DEFERRED' else: query += ' IMMEDIATE' return query def get_alter_table_sql(self, diff): """ Get the ALTER TABLE SQL statement :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: list """ sql = self._get_simple_alter_table_sql(diff) if sql is not False: return sql from_table = diff.from_table if not isinstance(from_table, Table): raise DBALException( 'SQLite platform requires for the alter table the table diff ' 'referencing the original table' ) table = from_table.clone() columns = OrderedDict() old_column_names = OrderedDict() new_column_names = OrderedDict() column_sql = [] for column_name, column in table.get_columns().items(): column_name = column_name.lower() columns[column_name] = column old_column_names[column_name] = column.get_quoted_name(self) new_column_names[column_name] = column.get_quoted_name(self) for column_name, column in diff.removed_columns.items(): column_name = column_name.lower() if column_name in columns: del columns[column_name] del old_column_names[column_name] del new_column_names[column_name] for old_column_name, column in diff.renamed_columns.items(): old_column_name = old_column_name.lower() if old_column_name in columns: del columns[old_column_name] columns[column.get_name().lower()] = column if old_column_name in new_column_names: new_column_names[old_column_name] = column.get_quoted_name(self) for old_column_name, column_diff in diff.changed_columns.items(): if old_column_name in columns: del columns[old_column_name] columns[column_diff.column.get_name().lower()] = column_diff.column if old_column_name in new_column_names: new_column_names[old_column_name] = column_diff.column.get_quoted_name(self) for column_name, column in diff.added_columns.items(): columns[column_name.lower()] = column table_sql = [] data_table = Table('__temp__' + table.get_name()) new_table = Table(table.get_quoted_name(self), columns, self._get_primary_index_in_altered_table(diff), self._get_foreign_keys_in_altered_table(diff), table.get_options()) new_table.add_option('alter', True) sql = self.get_pre_alter_table_index_foreign_key_sql(diff) sql.append('CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s' % (data_table.get_quoted_name(self), ', '.join(old_column_names.values()), table.get_quoted_name(self))) sql.append(self.get_drop_table_sql(from_table)) sql += self.get_create_table_sql(new_table) sql.append('INSERT INTO %s (%s) SELECT %s FROM %s' % (new_table.get_quoted_name(self), ', '.join(new_column_names.values()), ', '.join(old_column_names.values()), data_table.get_name())) sql.append(self.get_drop_table_sql(data_table)) sql += self.get_post_alter_table_index_foreign_key_sql(diff) return sql def _get_simple_alter_table_sql(self, diff): for old_column_name, column_diff in diff.changed_columns.items(): if not isinstance(column_diff.from_column, Column)\ or not isinstance(column_diff.column, Column)\ or not column_diff.column.get_autoincrement()\ or column_diff.column.get_type().lower() != 'integer': continue if not column_diff.has_changed('type') and not column_diff.has_changed('unsigned'): del diff.changed_columns[old_column_name] continue from_column_type = column_diff.column.get_type() if from_column_type == 'smallint' or from_column_type == 'bigint': del diff.changed_columns[old_column_name] if any([not diff.renamed_columns, not diff.added_foreign_keys, not diff.added_indexes, not diff.changed_columns, not diff.changed_foreign_keys, not diff.changed_indexes, not diff.removed_columns, not diff.removed_foreign_keys, not diff.removed_indexes, not diff.renamed_indexes]): return False table = Table(diff.name) sql = [] table_sql = [] column_sql = [] for column in diff.added_columns.values(): field = { 'unique': None, 'autoincrement': None, 'default': None } field.update(column.to_dict()) type_ = field['type'] if 'column_definition' in field or field['autoincrement'] or field['unique']: return False elif type_ == 'datetime' and field['default'] == self.get_current_timestamp_sql(): return False elif type_ == 'date' and field['default'] == self.get_current_date_sql(): return False elif type_ == 'time' and field['default'] == self.get_current_time_sql(): return False field['name'] = column.get_quoted_name(self) if field['type'].lower() == 'string' and field['length'] is None: field['length'] = 255 sql.append('ALTER TABLE ' + table.get_quoted_name(self) + ' ADD COLUMN ' + self.get_column_declaration_sql(field['name'], field)) if diff.new_name is not False: new_table = Identifier(diff.new_name) sql.append('ALTER TABLE ' + table.get_quoted_name(self) + ' RENAME TO ' + new_table.get_quoted_name(self)) return sql def _get_indexes_in_altered_table(self, diff): """ :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: dict """ indexes = diff.from_table.get_indexes() column_names = self._get_column_names_in_altered_table(diff) for key, index in OrderedDict([(k, v) for k, v in indexes.items()]).items(): for old_index_name, renamed_index in diff.renamed_indexes.items(): if key.lower() == old_index_name.lower(): del indexes[key] changed = False index_columns = [] for column_name in index.get_columns(): normalized_column_name = column_name.lower() if normalized_column_name not in column_names: del indexes[key] break else: index_columns.append(column_names[normalized_column_name]) if column_name != column_names[normalized_column_name]: changed = True if changed: indexes[key] = Index(index.get_name(), index_columns, index.is_unique(), index.is_primary(), index.get_flags()) for index in diff.removed_indexes.values(): index_name = index.get_name().lower() if index_name and index_name in indexes: del indexes[index_name] changed_indexes = ( list(diff.changed_indexes.values()) + list(diff.added_indexes.values()) + list(diff.renamed_indexes.values()) ) for index in changed_indexes: index_name = index.get_name().lower() if index_name: indexes[index_name] = index else: indexes[len(indexes)] = index return indexes def _get_column_names_in_altered_table(self, diff): """ :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: dict """ columns = OrderedDict() for column_name, column in diff.from_table.get_columns().items(): columns[column_name.lower()] = column.get_name() for column_name, column in diff.removed_columns.items(): column_name = column_name.lower() if column_name in columns: del columns[column_name] for old_column_name, column in diff.renamed_columns.items(): column_name = column.get_name() columns[old_column_name.lower()] = column_name columns[column_name.lower()] = column_name for old_column_name, column_diff in diff.changed_columns.items(): column_name = column_diff.column.get_name() columns[old_column_name.lower()] = column_name columns[column_name.lower()] = column_name for column_name, column in diff.added_columns.items(): columns[column_name.lower()] = column_name return columns def _get_foreign_keys_in_altered_table(self, diff): """ :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: dict """ foreign_keys = diff.from_table.get_foreign_keys() column_names = self._get_column_names_in_altered_table(diff) for key, constraint in foreign_keys.items(): changed = False local_columns = [] for column_name in constraint.get_local_columns(): normalized_column_name = column_name.lower() if normalized_column_name not in column_names: del foreign_keys[key] break else: local_columns.append(column_names[normalized_column_name]) if column_name != column_names[normalized_column_name]: changed = True if changed: foreign_keys[key] = ForeignKeyConstraint( local_columns, constraint.get_foreign_table_name(), constraint.get_foreign_columns(), constraint.get_name(), constraint.get_options() ) for constraint in diff.removed_foreign_keys: constraint_name = constraint.get_name().lower() if constraint_name and constraint_name in foreign_keys: del foreign_keys[constraint_name] foreign_keys_diff = diff.changed_foreign_keys + diff.added_foreign_keys for constraint in foreign_keys_diff: constraint_name = constraint.get_name().lower() if constraint_name: foreign_keys[constraint_name] = constraint else: foreign_keys[len(foreign_keys)] = constraint return foreign_keys def _get_primary_index_in_altered_table(self, diff): """ :param diff: The table diff :type diff: orator.dbal.table_diff.TableDiff :rtype: dict """ primary_index = {} for index in self._get_indexes_in_altered_table(diff).values(): if index.is_primary(): primary_index = {index.get_name(): index} return primary_index def supports_foreign_key_constraints(self): return True def get_boolean_type_declaration_sql(self, column): return 'BOOLEAN' def get_integer_type_declaration_sql(self, column): return 'INTEGER' + self._get_common_integer_type_declaration_sql(column) def get_bigint_type_declaration_sql(self, column): # SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT fields. if not column.get('autoincrement', False): return self.get_integer_type_declaration_sql(column) return 'BIGINT' + self._get_common_integer_type_declaration_sql(column) def get_tinyint_type_declaration_sql(self, column): # SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT fields. if not column.get('autoincrement', False): return self.get_integer_type_declaration_sql(column) return 'TINYINT' + self._get_common_integer_type_declaration_sql(column) def get_smallint_type_declaration_sql(self, column): # SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT fields. if not column.get('autoincrement', False): return self.get_integer_type_declaration_sql(column) return 'SMALLINT' + self._get_common_integer_type_declaration_sql(column) def get_mediumint_type_declaration_sql(self, column): # SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT fields. if not column.get('autoincrement', False): return self.get_integer_type_declaration_sql(column) return 'MEDIUMINT' + self._get_common_integer_type_declaration_sql(column) def get_datetime_type_declaration_sql(self, column): return 'DATETIME' def get_date_type_declaration_sql(self, column): return 'DATE' def get_time_type_declaration_sql(self, column): return 'TIME' def _get_common_integer_type_declaration_sql(self, column): # sqlite autoincrement is implicit for integer PKs, but not when the field is unsigned if not column.get('autoincrement', False): return '' if not column.get('unsigned', False): return ' UNSIGNED' return '' def get_varchar_type_declaration_sql_snippet(self, length, fixed): if fixed: return 'CHAR(%s)' % length if length else 'CHAR(255)' else: return 'VARCHAR(%s)' % length if length else 'TEXT' def get_blob_type_declaration_sql(self, column): return 'BLOB' def get_clob_type_declaration_sql(self, column): return 'CLOB' def get_column_options(self): return ['pk'] def _get_reserved_keywords_class(self): return SQLiteKeywords PK!Q&orator/dbal/postgres_schema_manager.py# -*- coding: utf-8 -*- import re from .column import Column from .foreign_key_constraint import ForeignKeyConstraint from .schema_manager import SchemaManager class PostgresSchemaManager(SchemaManager): def _get_portable_table_column_definition(self, table_column): if table_column['type'].lower() == 'varchar' or table_column['type'] == 'bpchar': length = re.sub('.*\(([0-9]*)\).*', '\\1', table_column['complete_type']) table_column['length'] = length autoincrement = False match = re.match("^nextval\('?(.*)'?(::.*)?\)$", str(table_column['default'])) if match: table_column['sequence'] = match.group(1) table_column['default'] = None autoincrement = True match = re.match("^'?([^']*)'?::.*$", str(table_column['default'])) if match: table_column['default'] = match.group(1) if str(table_column['default']).find('NULL') == 0: table_column['default'] = None if 'length' in table_column: length = table_column['length'] else: length = None if length == '-1' and 'atttypmod' in table_column: length = table_column['atttypmod'] - 4 if length is None or not length.isdigit() or int(length) <= 0: length = None fixed = None if 'name' not in table_column: table_column['name'] = '' precision = None scale = None db_type = table_column['type'].lower() type = self._platform.get_type_mapping(db_type) if db_type in ['smallint', 'int2']: length = None elif db_type in ['int', 'int4', 'integer']: length = None elif db_type in ['int8', 'bigint']: length = None elif db_type in ['bool', 'boolean']: if table_column['default'] == 'true': table_column['default'] = True if table_column['default'] == 'false': table_column['default'] = False length = None elif db_type == 'text': fixed = False elif db_type in ['varchar', 'interval', '_varchar']: fixed = False elif db_type in ['char', 'bpchar']: fixed = True elif db_type in ['float', 'float4', 'float8', 'double', 'double precision', 'real', 'decimal', 'money', 'numeric']: match = re.match('([A-Za-z]+\(([0-9]+),([0-9]+)\))', table_column['complete_type']) if match: precision = match.group(1) scale = match.group(2) length = None elif db_type == 'year': length = None if table_column['default']: match = re.match("('?([^']+)'?::)", str(table_column['default'])) if match: table_column['default'] = match.group(1) options = { 'length': length, 'notnull': table_column['isnotnull'], 'default': table_column['default'], 'primary': table_column['pri'] == 't', 'precision': precision, 'scale': scale, 'fixed': fixed, 'unsigned': False, 'autoincrement': autoincrement } column = Column(table_column['field'], type, options) return column def _get_portable_table_indexes_list(self, table_indexes, table_name): buffer = [] for row in table_indexes: col_numbers = row['indkey'].split(' ') col_numbers_sql = 'IN (%s)' % ', '.join(col_numbers) column_name_sql = 'SELECT attnum, attname FROM pg_attribute ' \ 'WHERE attrelid=%s AND attnum %s ORDER BY attnum ASC;'\ % (row['indrelid'], col_numbers_sql) index_columns = self._connection.select(column_name_sql) # required for getting the order of the columns right. for col_num in col_numbers: for col_row in index_columns: if int(col_num) == col_row['attnum']: buffer.append({ 'key_name': row['relname'], 'column_name': col_row['attname'].strip(), 'non_unique': not row['indisunique'], 'primary': row['indisprimary'], 'where': row['where'] }) return super(PostgresSchemaManager, self)._get_portable_table_indexes_list(buffer, table_name) def _get_portable_table_foreign_key_definition(self, table_foreign_key): on_update = '' on_delete = '' match = re.match('ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?)', table_foreign_key['condef']) if match: on_update = match.group(1) match = re.match('ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?)', table_foreign_key['condef']) if match: on_delete = match.group(1) values = re.match('FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)', table_foreign_key['condef']) if values: local_columns = [c.strip() for c in values.group(1).split(',')] foreign_columns = [c.strip() for c in values.group(3).split(',')] foreign_table = values.group(2) return ForeignKeyConstraint( local_columns, foreign_table, foreign_columns, table_foreign_key['conname'], {'on_update': on_update, 'on_delete': on_delete} ) PK!.__orator/dbal/schema_manager.py# -*- coding: utf-8 -*- from collections import OrderedDict from .table import Table from .column import Column from .index import Index class SchemaManager(object): def __init__(self, connection, platform=None): """ :param connection: The connection to use :type connection: orator.connection.Connection :param platform: The platform :type platform: orator.dbal.platforms.Platform """ self._connection = connection if not platform: self._platform = self._connection.get_database_platform() else: self._platform = platform def list_table_columns(self, table): sql = self._platform.get_list_table_columns_sql(table) cursor = self._connection.get_connection().cursor() cursor.execute(sql) table_columns = map(lambda x: dict(x.items()), cursor.fetchall()) return self._get_portable_table_columns_list(table, table_columns) def list_table_indexes(self, table): sql = self._platform.get_list_table_indexes_sql(table) table_indexes = self._connection.select(sql) return self._get_portable_table_indexes_list(table_indexes, table) def list_table_foreign_keys(self, table): sql = self._platform.get_list_table_foreign_keys_sql(table) table_foreign_keys = self._connection.select(sql) return self._get_portable_table_foreign_keys_list(table_foreign_keys) def list_table_details(self, table_name): columns = self.list_table_columns(table_name) foreign_keys = [] if self._platform.supports_foreign_key_constraints(): foreign_keys = self.list_table_foreign_keys(table_name) indexes = self.list_table_indexes(table_name) table = Table(table_name, columns, indexes, foreign_keys) return table def _get_portable_table_columns_list(self, table, table_columns): columns_list = OrderedDict() for table_column in table_columns: column = self._get_portable_table_column_definition(table_column) if column: name = column.get_name().lower() columns_list[name] = column return columns_list def _get_portable_table_column_definition(self, table_column): raise NotImplementedError def _get_portable_table_indexes_list(self, table_indexes, table_name): result = OrderedDict() for table_index in table_indexes: index_name = table_index['key_name'] key_name = table_index['key_name'] if table_index['primary']: key_name = 'primary' key_name = key_name.lower() if key_name not in result: options = {} if 'where' in table_index: options['where'] = table_index['where'] result[key_name] = { 'name': index_name, 'columns': [table_index['column_name']], 'unique': not table_index['non_unique'], 'primary': table_index['primary'], 'flags': table_index.get('flags') or None, 'options': options } else: result[key_name]['columns'].append(table_index['column_name']) indexes = OrderedDict() for index_key, data in result.items(): index = Index( data['name'], data['columns'], data['unique'], data['primary'], data['flags'], data['options'] ) indexes[index_key] = index return indexes def _get_portable_table_foreign_keys_list(self, table_foreign_keys): foreign_keys = [] for value in table_foreign_keys: value = self._get_portable_table_foreign_key_definition(value) if value: foreign_keys.append(value) return foreign_keys def _get_portable_table_foreign_key_definition(self, table_foreign_key): return table_foreign_key def get_database_platform(self): return self._connection.get_database_platform() PK!NƱ$orator/dbal/sqlite_schema_manager.py# -*- coding: utf-8 -*- import re from collections import OrderedDict from .schema_manager import SchemaManager from .column import Column from .foreign_key_constraint import ForeignKeyConstraint class SQLiteSchemaManager(SchemaManager): def _get_portable_table_column_definition(self, table_column): parts = table_column['type'].split('(') table_column['type'] = parts[0] if len(parts) > 1: length = parts[1].strip(')') table_column['length'] = length db_type = table_column['type'].lower() length = table_column.get('length', None) unsigned = False if ' unsigned' in db_type: db_type = db_type.replace(' unsigned', '') unsigned = True fixed = False type = self._platform.get_type_mapping(db_type) default = table_column['dflt_value'] if default == 'NULL': default = None if default is not None: # SQLite returns strings wrapped in single quotes, so we need to strip them default = re.sub("^'(.*)'$", '\\1', default) notnull = bool(table_column['notnull']) if 'name' not in table_column: table_column['name'] = '' precision = None scale = None if db_type in ['char']: fixed = True elif db_type in ['varchar']: length = length or 255 elif db_type in ['float', 'double', 'real', 'decimal', 'numeric']: if 'length' in table_column: if ',' not in table_column['length']: table_column['length'] += ',0' precision, scale = tuple(map(lambda x: x.strip(), table_column['length'].split(','))) length = None options = { 'length': length, 'unsigned': bool(unsigned), 'fixed': fixed, 'notnull': notnull, 'default': default, 'precision': precision, 'scale': scale, 'autoincrement': False } column = Column(table_column['name'], type, options) column.set_platform_option('pk', table_column['pk']) return column def _get_portable_table_indexes_list(self, table_indexes, table_name): index_buffer = [] # Fetch primary info = self._connection.select('PRAGMA TABLE_INFO (%s)' % table_name) for row in info: if row['pk'] != 0: index_buffer.append({ 'key_name': 'primary', 'primary': True, 'non_unique': False, 'column_name': row['name'] }) # Fetch regular indexes for index in table_indexes: # Ignore indexes with reserved names, e.g. autoindexes if index['name'].find('sqlite_') == -1: key_name = index['name'] idx = { 'key_name': key_name, 'primary': False, 'non_unique': not bool(index['unique']) } info = self._connection.select('PRAGMA INDEX_INFO (\'%s\')' % key_name) for row in info: idx['column_name'] = row['name'] index_buffer.append(idx) return super(SQLiteSchemaManager, self)._get_portable_table_indexes_list(index_buffer, table_name) def _get_portable_table_foreign_keys_list(self, table_foreign_keys): foreign_keys = OrderedDict() for value in table_foreign_keys: value = dict((k.lower(), v) for k, v in value.items()) name = value.get('constraint_name', None) if name is None: name = '%s_%s_%s' % (value['from'], value['table'], value['to']) if name not in foreign_keys: if 'on_delete' not in value or value['on_delete'] == 'RESTRICT': value['on_delete'] = None if 'on_update' not in value or value['on_update'] == 'RESTRICT': value['on_update'] = None foreign_keys[name] = { 'name': name, 'local': [], 'foreign': [], 'foreign_table': value['table'], 'on_delete': value['on_delete'], 'on_update': value['on_update'], 'deferrable': value.get('deferrable', False), 'deferred': value.get('deferred', False) } foreign_keys[name]['local'].append(value['from']) foreign_keys[name]['foreign'].append(value['to']) result = [] for constraint in foreign_keys.values(): result.append( ForeignKeyConstraint( constraint['local'], constraint['foreign_table'], constraint['foreign'], constraint['name'], { 'on_delete': constraint['on_delete'], 'on_update': constraint['on_update'], 'deferrable': constraint['deferrable'], 'deferred': constraint['deferred'] } ) ) return result PK!9rVdAdAorator/dbal/table.py# -*- coding: utf-8 -*- import re from collections import OrderedDict from .column import Column from .abstract_asset import AbstractAsset from .index import Index from .foreign_key_constraint import ForeignKeyConstraint from .exceptions import ( DBALException, IndexDoesNotExist, IndexAlreadyExists, IndexNameInvalid, ColumnDoesNotExist, ColumnAlreadyExists, ForeignKeyDoesNotExist ) class Table(AbstractAsset): def __init__(self, table_name, columns=None, indexes=None, fk_constraints=None, options=None): self._set_name(table_name) self._primary_key_name = False self._columns = OrderedDict() self._indexes = OrderedDict() self._implicit_indexes = OrderedDict() self._fk_constraints = OrderedDict() self._options = options or {} columns = columns or [] indexes = indexes or [] fk_constraints = fk_constraints or [] columns = columns.values() if isinstance(columns, dict) else columns for column in columns: self._add_column(column) indexes = indexes.values() if isinstance(indexes, dict) else indexes for index in indexes: self._add_index(index) fk_constraints = fk_constraints.values() if isinstance(fk_constraints, dict) else fk_constraints for constraint in fk_constraints: self._add_foreign_key_constraint(constraint) def _get_max_identifier_length(self): return 63 def set_primary_key(self, columns, index_name=False): """ Set the primary key. :type columns: list :type index_name: str or bool :rtype: Table """ self._add_index(self._create_index(columns, index_name or 'primary', True, True)) for column_name in columns: column = self.get_column(column_name) column.set_notnull(True) return self def add_index(self, columns, name=None, flags=None, options=None): if not name: name = self._generate_identifier_name( [self.get_name()] + columns, 'idx', self._get_max_identifier_length() ) return self._add_index(self._create_index(columns, name, False, False, flags, options)) def drop_primary_key(self): """ Drop the primary key from this table. """ self.drop_index(self._primary_key_name) self._primary_key_name = False def drop_index(self, name): """ Drops an index from this table. :param name: The index name :type name: str """ name = self._normalize_identifier(name) if not self.has_index(name): raise IndexDoesNotExist(name, self._name) del self._indexes[name] def add_unique_index(self, columns, name=None, options=None): if not name: name = self._generate_identifier_name( [self.get_name()] + columns, 'uniq', self._get_max_identifier_length() ) return self._add_index(self._create_index(columns, name, True, False, None, options)) def rename_index(self, old_name, new_name=None): """ Renames an index. :param old_name: The name of the index to rename from. :type old_name: str :param new_name: The name of the index to rename to. :type new_name: str or None :rtype: Table """ old_name = self._normalize_identifier(old_name) normalized_new_name = self._normalize_identifier(new_name) if old_name == normalized_new_name: return self if not self.has_index(old_name): raise IndexDoesNotExist(old_name, self._name) if self.has_index(normalized_new_name): raise IndexAlreadyExists(normalized_new_name, self._name) old_index = self._indexes[old_name] if old_index.is_primary(): self.drop_primary_key() return self.set_primary_key(old_index.get_columns(), new_name) del self._indexes[old_name] if old_index.is_unique(): return self.add_unique_index(old_index.get_columns(), new_name) return self.add_index(old_index.get_columns(), new_name, old_index.get_flags()) def columns_are_indexed(self, columns): """ Checks if an index begins in the order of the given columns. :type columns: list :rtype: bool """ for index in self._indexes.values(): if index.spans_columns(columns): return True return False def _create_index(self, columns, name, is_unique, is_primary, flags=None, options=None): """ Creates an Index instance. :param columns: The index columns :type columns: list :param name: The index name :type name: str :param is_unique: Whether the index is unique or not :type is_unique: bool :param is_primary: Whether the index is primary or not :type is_primary: bool :param flags: The index flags :type: dict :param options: The options :type options: dict :rtype: Index """ if re.match('[^a-zA-Z0-9_]+', self._normalize_identifier(name)): raise IndexNameInvalid(name) for column in columns: if isinstance(column, dict): column = list(column.keys())[0] if not self.has_column(column): raise ColumnDoesNotExist(column, self._name) return Index(name, columns, is_unique, is_primary, flags, options) def add_column(self, name, type_name, options=None): """ Adds a new column. :param name: The column name :type name: str :param type_name: The column type :type type_name: str :param options: The column options :type options: dict :rtype: Column """ column = Column(name, type_name, options) self._add_column(column) return column def change_column(self, name, options): """ Changes column details. :param name: The column to change. :type name: str :param options: The new options. :type options: str :rtype: Table """ column = self.get_column(name) column.set_options(options) return self def drop_column(self, name): """ Drops a Column from the Table :param name: The name of the column :type name: str :rtype: Table """ name = self._normalize_identifier(name) del self._columns[name] return self def add_foreign_key_constraint(self, foreign_table, local_columns, foreign_columns, options=None, constraint_name=None): """ Adds a foreign key constraint. Name is inferred from the local columns. :param foreign_table: Table instance or table name :type foreign_table: Table or str :type local_columns: list :type foreign_columns: list :type options: dict :type constraint_name: str or None :rtype: Table """ if not constraint_name: constraint_name = self._generate_identifier_name( [self.get_name()] + local_columns, 'fk', self._get_max_identifier_length() ) return self.add_named_foreign_key_constraint( constraint_name, foreign_table, local_columns, foreign_columns, options ) def add_named_foreign_key_constraint(self, name, foreign_table, local_columns, foreign_columns, options): """ Adds a foreign key constraint with a given name. :param name: The constraint name :type name: str :param foreign_table: Table instance or table name :type foreign_table: Table or str :type local_columns: list :type foreign_columns: list :type options: dict :rtype: Table """ if isinstance(foreign_table, Table): for column in foreign_columns: if not foreign_table.has_column(column): raise ColumnDoesNotExist(column, foreign_table.get_name()) for column in local_columns: if not self.has_column(column): raise ColumnDoesNotExist(column, self._name) constraint = ForeignKeyConstraint( local_columns, foreign_table, foreign_columns, name, options ) self._add_foreign_key_constraint(constraint) return self def add_option(self, name, value): self._options[name] = value def _add_column(self, column): column_name = self._normalize_identifier(column.get_name()) if column_name in self._columns: raise ColumnAlreadyExists(column_name, self._name) self._columns[column_name] = column return self def _add_index(self, index): """ Adds an index to the table. :param index: The index to add :type index: Index :rtype: Table """ index_name = index.get_name() index_name = self._normalize_identifier(index_name) replaced_implicit_indexes = [] for name, implicit_index in self._implicit_indexes.items(): if implicit_index.is_fullfilled_by(index) and name in self._indexes: replaced_implicit_indexes.append(name) already_exists = ( index_name in self._indexes and index_name not in replaced_implicit_indexes or self._primary_key_name is not False and index.is_primary() ) if already_exists: raise IndexAlreadyExists(index_name, self._name) for name in replaced_implicit_indexes: del self._indexes[name] del self._implicit_indexes[name] if index.is_primary(): self._primary_key_name = index_name self._indexes[index_name] = index return self def _add_foreign_key_constraint(self, constraint): """ Adds a foreign key constraint. :param constraint: The constraint to add :type constraint: ForeignKeyConstraint :rtype: Table """ constraint.set_local_table(self) if constraint.get_name(): name = constraint.get_name() else: name = self._generate_identifier_name( [self.get_name()] + constraint.get_local_columns(), 'fk', self._get_max_identifier_length() ) name = self._normalize_identifier(name) self._fk_constraints[name] = constraint # Add an explicit index on the foreign key columns. # If there is already an index that fulfils this requirements drop the request. # In the case of __init__ calling this method during hydration from schema-details # all the explicitly added indexes lead to duplicates. # This creates computation overhead in this case, however no duplicate indexes # are ever added (based on columns). index_name = self._generate_identifier_name( [self.get_name()] + constraint.get_columns(), 'idx', self._get_max_identifier_length() ) index_candidate = self._create_index(constraint.get_columns(), index_name, False, False) for existing_index in self._indexes.values(): if index_candidate.is_fullfilled_by(existing_index): return #self._add_index(index_candidate) #self._implicit_indexes[self._normalize_identifier(index_name)] = index_candidate return self def has_foreign_key(self, name): """ Returns whether this table has a foreign key constraint with the given name. :param name: The constraint name :type name: str :rtype: bool """ name = self._normalize_identifier(name) return name in self._fk_constraints def get_foreign_key(self, name): """ Returns the foreign key constraint with the given name. :param name: The constraint name :type name: str :rtype: ForeignKeyConstraint """ name = self._normalize_identifier(name) if not self.has_foreign_key(name): raise ForeignKeyDoesNotExist(name, self._name) return self._fk_constraints[name] def remove_foreign_key(self, name): """ Removes the foreign key constraint with the given name. :param name: The constraint name :type name: str """ name = self._normalize_identifier(name) if not self.has_foreign_key(name): raise ForeignKeyDoesNotExist(name, self._name) del self._fk_constraints[name] def get_columns(self): columns = self._columns pk_cols = [] fk_cols = [] if self.has_primary_key(): pk_cols = self.get_primary_key().get_columns() for fk in self.get_foreign_keys().values(): fk_cols += fk.get_columns() col_names = pk_cols + fk_cols col_names = [x for x in col_names if x not in columns] col_names += list(columns.keys()) return columns def has_column(self, column): return self._normalize_identifier(column) in self._columns def get_column(self, column): column = self._normalize_identifier(column) if not self.has_column(column): raise ColumnDoesNotExist(column, self._name) return self._columns[column] def get_primary_key(self): """ Returns the primary key :rtype: Index or None """ if not self.has_primary_key(): return None return self.get_index(self._primary_key_name) def get_primary_key_columns(self): """ Returns the primary key columns. :rtype: list """ if not self.has_primary_key(): raise DBALException('Table "%s" has no primary key.' % self.get_name()) return self.get_primary_key().get_columns() def has_primary_key(self): """ Returns whether this table has a primary key. :rtype: bool """ if not self._primary_key_name: return False return self.has_index(self._primary_key_name) def has_index(self, name): """ Returns whether this table has an Index with the given name. :param name: The index name :type name: str :rtype: bool """ name = self._normalize_identifier(name) return name in self._indexes def get_index(self, name): """ Returns the Index with the given name. :param name: The index name :type name: str :rtype: Index """ name = self._normalize_identifier(name) if not self.has_index(name): raise IndexDoesNotExist(name, self._name) return self._indexes[name] def get_indexes(self): return self._indexes def get_foreign_keys(self): return self._fk_constraints def has_option(self, name): return name in self._options def get_option(self, name): return self._options[name] def get_options(self): return self._options def get_name(self): return self._name def clone(self): table = Table(self._name) table._primary_key_name = self._primary_key_name for k, column in self._columns.items(): table._columns[k] = Column(column.get_name(), column.get_type(), column.to_dict()) for k, index in self._indexes.items(): table._indexes[k] = Index( index.get_name(), index.get_columns(), index.is_unique(), index.is_primary(), index.get_flags(), index.get_options() ) for k, fk in self._fk_constraints.items(): table._fk_constraints[k] = ForeignKeyConstraint( fk.get_local_columns(), fk.get_foreign_table_name(), fk.get_foreign_columns(), fk.get_name(), fk.get_options() ) table._fk_constraints[k].set_local_table(table) return table def _normalize_identifier(self, identifier): """ Normalizes a given identifier. Trims quotes and lowercases the given identifier. :param identifier: The identifier to normalize. :type identifier: str :rtype: str """ return self._trim_quotes(identifier.lower()) PK!.wpporator/dbal/table_diff.py# -*- coding: utf-8 -*- from collections import OrderedDict from .table import Table from .identifier import Identifier class TableDiff(object): def __init__(self, table_name, added_columns=None, changed_columns=None, removed_columns=None, added_indexes=None, changed_indexes=None, removed_indexes=None, from_table=None): self.name = table_name self.new_name = False self.added_columns = added_columns or OrderedDict() self.changed_columns = changed_columns or OrderedDict() self.removed_columns = removed_columns or OrderedDict() self.added_indexes = added_indexes or OrderedDict() self.changed_indexes = changed_indexes or OrderedDict() self.removed_indexes = removed_indexes or OrderedDict() self.added_foreign_keys = [] self.changed_foreign_keys = [] self.removed_foreign_keys = [] self.renamed_columns = OrderedDict() self.renamed_indexes = OrderedDict() self.from_table = from_table def get_name(self, platform): if isinstance(self.from_table, Table): name = self.from_table.get_quoted_name(platform) else: name = self.name return Identifier(name) def get_new_name(self): if self.new_name: return Identifier(self.new_name) return self.new_name PK!orator/dbal/types/__init__.py# -*- coding: utf-8 -*- PK!Ꮍorator/events/__init__.py# -*- coding: utf-8 -*- from blinker import Namespace class Event(object): events = Namespace() @classmethod def fire(cls, name, *args, **kwargs): name = 'orator.%s' % name signal = cls.events.signal(name) for response in signal.send(*args, **kwargs): if response[1] is False: return False @classmethod def listen(cls, name, callback, *args, **kwargs): name = 'orator.%s' % name signal = cls.events.signal(name) signal.connect(callback, weak=False, *args, **kwargs) @classmethod def forget(cls, name, *args, **kwargs): name = 'orator.%s' % name signal = cls.events.signal(name) for receiver in signal.receivers: signal.disconnect(receiver, *args, **kwargs) def event(name, *args, **kwargs): return Event.fire(name, *args, **kwargs) def listen(name, callback, *args, **kwargs): return Event.listen(name, callback, *args, **kwargs) PK!ySDDorator/exceptions/__init__.py# -*- coding: utf-8 -*- class ArgumentError(Exception): pass PK!(V-orator/exceptions/connection.py# -*- coding: utf-8 -*- class TransactionError(ConnectionError): def __init__(self, previous, message=None): self.previous = previous self.message = 'Transaction Error: ' PK!T&&orator/exceptions/connectors.py# -*- coding: utf-8 -*- class ConnectorException(Exception): pass class UnsupportedDriver(ConnectorException): def __init__(self, driver): message = 'Driver "%s" is not supported' % driver super(UnsupportedDriver, self).__init__(message) class MissingPackage(ConnectorException): def __init__(self, driver, supported_packages): if not isinstance(supported_packages, list): supported_packages = [supported_packages] message = 'Driver "%s" requires ' % driver if len(supported_packages) == 1: message += '"%s" package' % supported_packages[0] else: message += 'one of the following packages: "%s"' % ('", "'.join(supported_packages)) super(MissingPackage, self).__init__(message) PK!))orator/exceptions/orm.py# -*- coding: utf-8 -*- class ModelNotFound(RuntimeError): def __init__(self, model): self._model = model self.message = 'No query results found for model [%s]' % self._model.__name__ def __str__(self): return self.message class MassAssignmentError(RuntimeError): pass class RelatedClassNotFound(RuntimeError): def __init__(self, related): self._related = related self.message = 'The related class for "%s" does not exists' % related def __str__(self): return self.message PK!rI#orator/exceptions/query.py# -*- coding: utf-8 -*- class QueryException(Exception): def __init__(self, sql, bindings, previous): self.sql = sql self.bindings = bindings self.previous = previous self.message = self.format_message(sql, bindings, previous) def format_message(self, sql, bindings, previous): return '%s (SQL: %s (%s))' % (str(previous), sql, bindings) def __repr__(self): return self.message def __str__(self): return self.message PK!Norator/migrations/__init__.py# -*- coding: utf-8 -*- from .database_migration_repository import DatabaseMigrationRepository from .migration_creator import MigrationCreator from .migration import Migration from .migrator import Migrator PK!1g 2orator/migrations/database_migration_repository.py# -*- coding: utf-8 -*- from .migration import Migration class DatabaseMigrationRepository(object): def __init__(self, resolver, table): """ :type resolver: orator.database_manager.DatabaseManager :type table: str """ self._resolver = resolver self._table = table self._connection = None def get_ran(self): """ Get the ran migrations. :rtype: list """ return self.table().lists('migration') def get_last(self): """ Get the last migration batch. :rtype: list """ query = self.table().where('batch', self.get_last_batch_number()) return query.order_by('migration', 'desc').get() def log(self, file, batch): """ Log that a migration was run. :type file: str :type batch: int """ record = { 'migration': file, 'batch': batch } self.table().insert(**record) def delete(self, migration): """ Remove a migration from the log. :type migration: dict """ self.table().where('migration', migration['migration']).delete() def get_next_batch_number(self): """ Get the next migration batch number. :rtype: int """ return self.get_last_batch_number() + 1 def get_last_batch_number(self): """ Get the last migration batch number. :rtype: int """ return self.table().max('batch') or 0 def create_repository(self): """ Create the migration repository data store. """ schema = self.get_connection().get_schema_builder() with schema.create(self._table) as table: # The migrations table is responsible for keeping track of which of the # migrations have actually run for the application. We'll create the # table to hold the migration file's path as well as the batch ID. table.string('migration') table.integer('batch') def repository_exists(self): """ Determine if the repository exists. :rtype: bool """ schema = self.get_connection().get_schema_builder() return schema.has_table(self._table) def table(self): """ Get a query builder for the migration table. :rtype: orator.query.builder.QueryBuilder """ return self.get_connection().table(self._table) def get_connection_resolver(self): return self._resolver def get_connection(self): return self._resolver.connection(self._connection) def set_source(self, name): self._connection = name PK!Uorator/migrations/migration.py# -*- coding: utf-8 -*- from orator import Model class Migration(object): _connection = None transactional = True @property def schema(self): return self._connection.get_schema_builder() @property def db(self): return self._connection def get_connection(self): return self._connection def set_connection(self, connection): self._connection = connection PK!҂  &orator/migrations/migration_creator.py# -*- coding: utf-8 -*- import os import inflection import datetime import errno from .stubs import CREATE_STUB, UPDATE_STUB, BLANK_STUB def mkdir_p(path): try: os.makedirs(path) except OSError as exc: if exc.errno == errno.EEXIST and os.path.isdir(path): pass else: raise class MigrationCreator(object): def create(self, name, path, table=None, create=False): """ Create a new migration at the given path. :param name: The name of the migration :type name: str :param path: The path of the migrations :type path: str :param table: The table name :type table: str :param create: Whether it's a create migration or not :type create: bool :rtype: str """ path = self._get_path(name, path) if not os.path.exists(os.path.dirname(path)): mkdir_p(os.path.dirname(path)) parent = os.path.join(os.path.dirname(path), '__init__.py') if not os.path.exists(parent): with open(parent, 'w'): pass stub = self._get_stub(table, create) with open(path, 'w') as fh: fh.write(self._populate_stub(name, stub, table)) return path def _get_stub(self, table, create): """ Get the migration stub template :param table: The table name :type table: str :param create: Whether it's a create migration or not :type create: bool :rtype: str """ if table is None: return BLANK_STUB else: if create: stub = CREATE_STUB else: stub = UPDATE_STUB return stub def _populate_stub(self, name, stub, table): """ Populate the placeholders in the migration stub. :param name: The name of the migration :type name: str :param stub: The stub :type stub: str :param table: The table name :type table: str :rtype: str """ stub = stub.replace('DummyClass', self._get_class_name(name)) if table is not None: stub = stub.replace('dummy_table', table) return stub def _get_class_name(self, name): return inflection.camelize(name) def _get_path(self, name, path): return os.path.join(path, self._get_date_prefix() + '_' + name + '.py') def _get_date_prefix(self): return datetime.datetime.utcnow().strftime('%Y_%m_%d_%H%M%S') PK!fFForator/migrations/migrator.py# -*- coding: utf-8 -*- import os import glob import inflection import logging from pygments import highlight from pygments.lexers.sql import SqlLexer from ..utils import decode, load_module from ..utils.command_formatter import CommandFormatter class MigratorHandler(logging.NullHandler): def __init__(self, level=logging.DEBUG): super(MigratorHandler, self).__init__(level) self.queries = [] def handle(self, record): self.queries.append(record.query) class Migrator(object): def __init__(self, repository, resolver): """ :type repository: DatabaseMigrationRepository :type resolver: orator.database_manager.DatabaseManager """ self._repository = repository self._resolver = resolver self._connection = None self._notes = [] def run(self, path, pretend=False): """ Run the outstanding migrations for a given path. :param path: The path :type path: str :param pretend: Whether we execute the migrations as dry-run :type pretend: bool """ self._notes = [] files = self._get_migration_files(path) ran = self._repository.get_ran() migrations = [f for f in files if f not in ran] self.run_migration_list(path, migrations, pretend) def run_migration_list(self, path, migrations, pretend=False): """ Run a list of migrations. :type migrations: list :type pretend: bool """ if not migrations: self._note('Nothing to migrate') return batch = self._repository.get_next_batch_number() for f in migrations: self._run_up(path, f, batch, pretend) def _run_up(self, path, migration_file, batch, pretend=False): """ Run "up" a migration instance. :type migration_file: str :type batch: int :type pretend: bool """ migration = self._resolve(path, migration_file) if pretend: return self._pretend_to_run(migration, 'up') if migration.transactional: with migration.db.transaction(): migration.up() else: migration.up() self._repository.log(migration_file, batch) self._note(decode('[OK] Migrated ') + '%s' % migration_file) def rollback(self, path, pretend=False): """ Rollback the last migration operation. :param path: The path :type path: str :param pretend: Whether we execute the migrations as dry-run :type pretend: bool :rtype: int """ self._notes = [] migrations = self._repository.get_last() if not migrations: self._note('Nothing to rollback.') return len(migrations) for migration in migrations: self._run_down(path, migration, pretend) return len(migrations) def reset(self, path, pretend=False): """ Rolls all of the currently applied migrations back. :param path: The path :type path: str :param pretend: Whether we execute the migrations as dry-run :type pretend: bool :rtype: count """ self._notes = [] migrations = sorted(self._repository.get_ran(), reverse=True) count = len(migrations) if count == 0: self._note('Nothing to rollback.') else: for migration in migrations: self._run_down(path, {'migration': migration}, pretend) return count def _run_down(self, path, migration, pretend=False): """ Run "down" a migration instance. """ migration_file = migration['migration'] instance = self._resolve(path, migration_file) if pretend: return self._pretend_to_run(instance, 'down') if instance.transactional: with instance.db.transaction(): instance.down() else: instance.down() self._repository.delete(migration) self._note(decode('[OK] Rolled back ') + '%s' % migration_file) def _get_migration_files(self, path): """ Get all of the migration files in a given path. :type path: str :rtype: list """ files = glob.glob(os.path.join(path, '[0-9]*_*.py')) if not files: return [] files = list(map(lambda f: os.path.basename(f).replace('.py', ''), files)) files = sorted(files) return files def _pretend_to_run(self, migration, method): """ Pretend to run the migration. :param migration: The migration :type migration: orator.migrations.migration.Migration :param method: The method to execute :type method: str """ self._note('') names = [] for query in self._get_queries(migration, method): name = migration.__class__.__name__ bindings = None if isinstance(query, tuple): query, bindings = query query = highlight( query, SqlLexer(), CommandFormatter() ).strip() if bindings: query = (query, bindings) if name not in names: self._note('[{}]'.format(name)) names.append(name) self._note(query) def _get_queries(self, migration, method): """ Get all of the queries that would be run for a migration. :param migration: The migration :type migration: orator.migrations.migration.Migration :param method: The method to execute :type method: str :rtype: list """ connection = migration.get_connection() db = connection with db.pretend(): getattr(migration, method)() return db.get_logged_queries() def _resolve(self, path, migration_file): """ Resolve a migration instance from a file. :param migration_file: The migration file :type migration_file: str :rtype: orator.migrations.migration.Migration """ name = '_'.join(migration_file.split('_')[4:]) migration_file = os.path.join(path, '%s.py' % migration_file) # Loading parent module parent = os.path.join(path, '__init__.py') if not os.path.exists(parent): with open(parent, 'w'): pass load_module('migrations', parent) # Loading module mod = load_module('migrations.%s' % name, migration_file) klass = getattr(mod, inflection.camelize(name)) instance = klass() instance.set_connection(self.get_repository().get_connection()) return instance def _note(self, message): """ Add a note to the migrator. :param message: The message :type message: str """ self._notes.append(message) def resolve_connection(self, connection): return self._resolver.connection(connection) def set_connection(self, name): if name is not None: self._resolver.set_default_connection(name) self._repository.set_source(name) self._connection = name def get_repository(self): return self._repository def repository_exists(self): return self._repository.repository_exists() def get_notes(self): return self._notes PK!9bZZorator/migrations/stubs.py# -*- coding: utf-8 -*- BLANK_STUB = """from orator.migrations import Migration class DummyClass(Migration): def up(self): \"\"\" Run the migrations. \"\"\" pass def down(self): \"\"\" Revert the migrations. \"\"\" pass """ CREATE_STUB = """from orator.migrations import Migration class DummyClass(Migration): def up(self): \"\"\" Run the migrations. \"\"\" with self.schema.create('dummy_table') as table: table.increments('id') table.timestamps() def down(self): \"\"\" Revert the migrations. \"\"\" self.schema.drop('dummy_table') """ UPDATE_STUB = """from orator.migrations import Migration class DummyClass(Migration): def up(self): \"\"\" Run the migrations. \"\"\" with self.schema.table('dummy_table') as table: pass def down(self): \"\"\" Revert the migrations. \"\"\" with self.schema.table('dummy_table') as table: pass """ PK!?porator/orm/__init__.py# -*- coding: utf-8 -*- from .builder import Builder from .model import Model from .mixins import SoftDeletes from .collection import Collection from .factory import Factory from .utils import ( mutator, accessor, column, has_one, morph_one, belongs_to, morph_to, has_many, has_many_through, morph_many, belongs_to_many, morph_to_many, morphed_by_many, scope ) PK!<}<}orator/orm/builder.py# -*- coding: utf-8 -*- import copy from collections import OrderedDict from ..exceptions.orm import ModelNotFound from ..utils import Null, basestring from ..query.expression import QueryExpression from ..pagination import Paginator, LengthAwarePaginator from ..support import Collection from .scopes import Scope class Builder(object): _passthru = [ 'to_sql', 'lists', 'insert', 'insert_get_id', 'pluck', 'count', 'min', 'max', 'avg', 'sum', 'exists', 'get_bindings', 'raw' ] def __init__(self, query): """ Constructor :param query: The underlying query builder :type query: QueryBuilder """ self._query = query self._model = None self._eager_load = {} self._macros = {} self._scopes = OrderedDict() self._on_delete = None def with_global_scope(self, identifier, scope): """ Register a new global scope. :param identifier: The scope's identifier :type identifier: str :param scope: The scope to register :type scope: Scope or callable :rtype: Builder """ self._scopes[identifier] = scope return self def without_global_scope(self, scope): """ Remove a registered global scope. :param scope: The scope to remove :type scope: Scope or str :rtype: Builder """ if isinstance(scope, basestring): del self._scopes[scope] return self keys = [] for key, value in self._scopes.items(): if scope == value.__class__ or isinstance(scope, value.__class__): keys.append(key) for key in keys: del self._scopes[key] return self def without_global_scopes(self): """ Remove all registered global scopes. :rtype: Builder """ self._scopes = OrderedDict() return self def find(self, id, columns=None): """ Find a model by its primary key :param id: The primary key value :type id: mixed :param columns: The columns to retrieve :type columns: list :return: The found model :rtype: orator.Model """ if columns is None: columns = ['*'] if isinstance(id, list): return self.find_many(id, columns) self._query.where(self._model.get_qualified_key_name(), '=', id) return self.first(columns) def find_many(self, id, columns=None): """ Find a model by its primary key :param id: The primary key values :type id: list :param columns: The columns to retrieve :type columns: list :return: The found model :rtype: orator.Collection """ if columns is None: columns = ['*'] if not id: return self._model.new_collection() self._query.where_in(self._model.get_qualified_key_name(), id) return self.get(columns) def find_or_fail(self, id, columns=None): """ Find a model by its primary key or raise an exception :param id: The primary key value :type id: mixed :param columns: The columns to retrieve :type columns: list :return: The found model :rtype: orator.Model :raises: ModelNotFound """ result = self.find(id, columns) if isinstance(id, list): if len(result) == len(set(id)): return result elif result: return result raise ModelNotFound(self._model.__class__) def first(self, columns=None): """ Execute the query and get the first result :param columns: The columns to get :type columns: list :return: The result :rtype: mixed """ if columns is None: columns = ['*'] return self.take(1).get(columns).first() def first_or_fail(self, columns=None): """ Execute the query and get the first result or raise an exception :param columns: The columns to get :type columns: list :return: The result :rtype: mixed """ model = self.first(columns) if model is not None: return model raise ModelNotFound(self._model.__class__) def get(self, columns=None): """ Execute the query as a "select" statement. :param columns: The columns to get :type columns: list :rtype: orator.Collection """ models = self.get_models(columns) # If we actually found models we will also eager load any relationships that # have been specified as needing to be eager loaded, which will solve the # n+1 query issue for the developers to avoid running a lot of queries. if len(models) > 0: models = self.eager_load_relations(models) collection = self._model.new_collection(models) return collection def pluck(self, column): """ Pluck a single column from the database. :param column: THe column to pluck :type column: str :return: The column value :rtype: mixed """ result = self.first([column]) if result: return result[column] def chunk(self, count): """ Chunk the results of the query :param count: The chunk size :type count: int :return: The current chunk :rtype: list """ connection = self._model.get_connection_name() for results in self.apply_scopes().get_query().chunk(count): models = self._model.hydrate(results, connection) # If we actually found models we will also eager load any relationships that # have been specified as needing to be eager loaded, which will solve the # n+1 query issue for the developers to avoid running a lot of queries. if len(models) > 0: models = self.eager_load_relations(models) collection = self._model.new_collection(models) yield collection def lists(self, column, key=None): """ Get a list with the values of a given column :param column: The column to get the values for :type column: str :param key: The key :type key: str :return: The list of values :rtype: list or dict """ results = self.to_base().lists(column, key) if not self._model.has_get_mutator(column): return results if isinstance(results, dict): for key, value in results.items(): fill = {column: value} results[key] = self._model.new_from_builder(fill).column else: for i, value in enumerate(results): fill = {column: value} results[i] = self._model.new_from_builder(fill).column def paginate(self, per_page=None, current_page=None, columns=None): """ Paginate the given query. :param per_page: The number of records per page :type per_page: int :param current_page: The current page of results :type current_page: int :param columns: The columns to return :type columns: list :return: The paginator """ if columns is None: columns = ['*'] total = self.to_base().get_count_for_pagination() page = current_page or Paginator.resolve_current_page() per_page = per_page or self._model.get_per_page() self._query.for_page(page, per_page) return LengthAwarePaginator(self.get(columns).all(), total, per_page, page) def simple_paginate(self, per_page=None, current_page=None, columns=None): """ Paginate the given query. :param per_page: The number of records per page :type per_page: int :param current_page: The current page of results :type current_page: int :param columns: The columns to return :type columns: list :return: The paginator """ if columns is None: columns = ['*'] page = current_page or Paginator.resolve_current_page() per_page = per_page or self._model.get_per_page() self.skip((page - 1) * per_page).take(per_page + 1) return Paginator(self.get(columns).all(), per_page, page) def update(self, _values=None, **values): """ Update a record in the database :param values: The values of the update :type values: dict :return: The number of records affected :rtype: int """ if _values is not None: values.update(_values) return self._query.update(self._add_updated_at_column(values)) def increment(self, column, amount=1, extras=None): """ Increment a column's value by a given amount :param column: The column to increment :type column: str :param amount: The amount by which to increment :type amount: int :param extras: Extra columns :type extras: dict :return: The number of rows affected :rtype: int """ if extras is None: extras = {} extras = self._add_updated_at_column(extras) return self.to_base().increment(column, amount, extras) def decrement(self, column, amount=1, extras=None): """ Decrement a column's value by a given amount :param column: The column to increment :type column: str :param amount: The amount by which to increment :type amount: int :param extras: Extra columns :type extras: dict :return: The number of rows affected :rtype: int """ if extras is None: extras = {} extras = self._add_updated_at_column(extras) return self.to_base().decrement(column, amount, extras) def _add_updated_at_column(self, values): """ Add the "updated_at" column to a dictionary of values. :param values: The values to update :type values: dict :return: The new dictionary of values :rtype: dict """ if not self._model.uses_timestamps(): return values column = self._model.get_updated_at_column() if 'updated_at' not in values: values.update({column: self._model.fresh_timestamp_string()}) return values def delete(self): """ Delete a record from the database. """ if self._on_delete is not None: return self._on_delete(self) return self._query.delete() def force_delete(self): """ Run the default delete function on the builder. """ return self._query.delete() def on_delete(self, callback): """ Register a replacement for the default delete function. :param callback: A replacement for the default delete function :type callback: callable """ self._on_delete = callback def get_models(self, columns=None): """ Get the hydrated models without eager loading. :param columns: The columns to get :type columns: list :return: A list of models :rtype: orator.orm.collection.Collection """ results = self.apply_scopes().get_query().get(columns).all() connection = self._model.get_connection_name() models = self._model.hydrate(results, connection) return models def eager_load_relations(self, models): """ Eager load the relationship of the models. :param models: :type models: list :return: The models :rtype: list """ for name, constraints in self._eager_load.items(): if name.find('.') == -1: models = self._load_relation(models, name, constraints) return models def _load_relation(self, models, name, constraints): """ Eagerly load the relationship on a set of models. :rtype: list """ relation = self.get_relation(name) relation.add_eager_constraints(models) if callable(constraints): constraints(relation.get_query()) else: relation.merge_query(constraints) models = relation.init_relation(models, name) results = relation.get_eager() return relation.match(models, results, name) def get_relation(self, relation): """ Get the relation instance for the given relation name. :rtype: orator.orm.relations.Relation """ from .relations import Relation with Relation.no_constraints(True): rel = getattr(self.get_model(), relation)() nested = self._nested_relations(relation) if len(nested) > 0: rel.get_query().with_(nested) return rel def _nested_relations(self, relation): """ Get the deeply nested relations for a given top-level relation. :rtype: dict """ nested = {} for name, constraints in self._eager_load.items(): if self._is_nested(name, relation): nested[name[len(relation + '.'):]] = constraints return nested def _is_nested(self, name, relation): """ Determine if the relationship is nested. :type name: str :type relation: str :rtype: bool """ dots = name.find('.') return dots and name.startswith(relation + '.') def where(self, column, operator=Null(), value=None, boolean='and'): """ Add a where clause to the query :param column: The column of the where clause, can also be a QueryBuilder instance for sub where :type column: str|Builder :param operator: The operator of the where clause :type operator: str :param value: The value of the where clause :type value: mixed :param boolean: The boolean of the where clause :type boolean: str :return: The current Builder instance :rtype: Builder """ if isinstance(column, Builder): self._query.add_nested_where_query(column.get_query(), boolean) else: self._query.where(column, operator, value, boolean) return self def or_where(self, column, operator=None, value=None): """ Add an "or where" clause to the query. :param column: The column of the where clause, can also be a QueryBuilder instance for sub where :type column: str or Builder :param operator: The operator of the where clause :type operator: str :param value: The value of the where clause :type value: mixed :return: The current Builder instance :rtype: Builder """ return self.where(column, operator, value, 'or') def where_exists(self, query, boolean='and', negate=False): """ Add an exists clause to the query. :param query: The exists query :type query: Builder or QueryBuilder :type boolean: str :type negate: bool :rtype: Builder """ if isinstance(query, Builder): query = query.get_query() self.get_query().where_exists(query, boolean, negate) return self def or_where_exists(self, query, negate=False): """ Add an or exists clause to the query. :param query: The exists query :type query: Builder or QueryBuilder :type negate: bool :rtype: Builder """ return self.where_exists(query, 'or', negate) def where_not_exists(self, query, boolean='and'): """ Add a where not exists clause to the query. :param query: The exists query :type query: Builder or QueryBuilder :type boolean: str :rtype: Builder """ return self.where_exists(query, boolean, True) def or_where_not_exists(self, query): """ Add a or where not exists clause to the query. :param query: The exists query :type query: Builder or QueryBuilder :rtype: Builder """ return self.or_where_exists(query, True) def has(self, relation, operator='>=', count=1, boolean='and', extra=None): """ Add a relationship count condition to the query. :param relation: The relation to count :type relation: str :param operator: The operator :type operator: str :param count: The count :type count: int :param boolean: The boolean value :type boolean: str :param extra: The extra query :type extra: Builder or callable :type: Builder """ if relation.find('.') >= 0: return self._has_nested(relation, operator, count, boolean, extra) relation = self._get_has_relation_query(relation) query = relation.get_relation_count_query(relation.get_related().new_query(), self) # TODO: extra query if extra: if callable(extra): extra(query) return self._add_has_where(query.apply_scopes(), relation, operator, count, boolean) def _has_nested(self, relations, operator='>=', count=1, boolean='and', extra=None): """ Add nested relationship count conditions to the query. :param relations: nested relations :type relations: str :param operator: The operator :type operator: str :param count: The count :type count: int :param boolean: The boolean value :type boolean: str :param extra: The extra query :type extra: Builder or callable :rtype: Builder """ relations = relations.split('.') def closure(q): if len(relations) > 1: q.where_has(relations.pop(0), closure) else: q.has(relations.pop(0), operator, count, boolean, extra) return self.where_has(relations.pop(0), closure) def doesnt_have(self, relation, boolean='and', extra=None): """ Add a relationship count to the query. :param relation: The relation to count :type relation: str :param boolean: The boolean value :type boolean: str :param extra: The extra query :type extra: Builder or callable :rtype: Builder """ return self.has(relation, '<', 1, boolean, extra) def where_has(self, relation, extra, operator='>=', count=1): """ Add a relationship count condition to the query with where clauses. :param relation: The relation to count :type relation: str :param extra: The extra query :type extra: Builder or callable :param operator: The operator :type operator: str :param count: The count :type count: int :rtype: Builder """ return self.has(relation, operator, count, 'and', extra) def where_doesnt_have(self, relation, extra=None): """ Add a relationship count condition to the query with where clauses. :param relation: The relation to count :type relation: str :param extra: The extra query :type extra: Builder or callable :rtype: Builder """ return self.doesnt_have(relation, 'and', extra) def or_has(self, relation, operator='>=', count=1): """ Add a relationship count condition to the query with an "or". :param relation: The relation to count :type relation: str :param operator: The operator :type operator: str :param count: The count :type count: int :rtype: Builder """ return self.has(relation, operator, count, 'or') def or_where_has(self, relation, extra, operator='>=', count=1): """ Add a relationship count condition to the query with where clauses and an "or". :param relation: The relation to count :type relation: str :param extra: The extra query :type extra: Builder or callable :param operator: The operator :type operator: str :param count: The count :type count: int :rtype: Builder """ return self.has(relation, operator, count, 'or', extra) def _add_has_where(self, has_query, relation, operator, count, boolean): """ Add the "has" condition where clause to the query. :param has_query: The has query :type has_query: Builder :param relation: The relation to count :type relation: orator.orm.relations.Relation :param operator: The operator :type operator: str :param count: The count :type count: int :param boolean: The boolean value :type boolean: str :rtype: Builder """ self._merge_model_defined_relation_wheres_to_has_query(has_query, relation) if isinstance(count, basestring) and count.isdigit(): count = QueryExpression(count) return self.where(QueryExpression('(%s)' % has_query.to_sql()), operator, count, boolean) def _merge_model_defined_relation_wheres_to_has_query(self, has_query, relation): """ Merge the "wheres" from a relation query to a has query. :param has_query: The has query :type has_query: Builder :param relation: The relation to count :type relation: orator.orm.relations.Relation """ relation_query = relation.get_base_query() has_query.merge_wheres( relation_query.wheres, relation_query.get_bindings() ) self._query.add_binding(has_query.get_query().get_bindings(), 'where') def _get_has_relation_query(self, relation): """ Get the "has" relation base query :type relation: str :rtype: Builder """ from .relations import Relation with Relation.no_constraints(True): return getattr(self.get_model(), relation)() def with_(self, *relations): """ Set the relationships that should be eager loaded. :return: The current Builder instance :rtype: Builder """ if not relations: return self eagers = self._parse_with_relations(list(relations)) self._eager_load.update(eagers) return self def _parse_with_relations(self, relations): """ Parse a list of relations into individuals. :param relations: The relation to parse :type relations: list :rtype: dict """ results = {} for relation in relations: if isinstance(relation, dict): name = list(relation.keys())[0] constraints = relation[name] else: name = relation constraints = self.__class__(self.get_query().new_query()) results = self._parse_nested_with(name, results) results[name] = constraints return results def _parse_nested_with(self, name, results): """ Parse the nested relationship in a relation. :param name: The name of the relationship :type name: str :type results: dict :rtype: dict """ progress = [] for segment in name.split('.'): progress.append(segment) last = '.'.join(progress) if last not in results: results[last] = self.__class__(self.get_query().new_query()) return results def _call_scope(self, scope, *args, **kwargs): """ Call the given model scope. :param scope: The scope to call :type scope: str """ query = self.get_query() # We will keep track of how many wheres are on the query before running the # scope so that we can properly group the added scope constraints in the # query as their own isolated nested where statement and avoid issues. original_where_count = len(query.wheres) result = getattr(self._model, scope)(self, *args, **kwargs) if self._should_nest_wheres_for_scope(query, original_where_count): self._nest_wheres_for_scope(query, [0, original_where_count, len(query.wheres)]) return result or self def apply_scopes(self): """ Get the underlying query builder instance with applied global scopes. :type: Builder """ if not self._scopes: return self builder = copy.copy(self) query = builder.get_query() # We will keep track of how many wheres are on the query before running the # scope so that we can properly group the added scope constraints in the # query as their own isolated nested where statement and avoid issues. original_where_count = len(query.wheres) where_counts = [0, original_where_count] for scope in self._scopes.values(): self._apply_scope(scope, builder) # Again, we will keep track of the count each time we add where clauses so that # we will properly isolate each set of scope constraints inside of their own # nested where clause to avoid any conflicts or issues with logical order. where_counts.append(len(query.wheres)) if self._should_nest_wheres_for_scope(query, original_where_count): self._nest_wheres_for_scope(query, Collection(where_counts).unique().all()) return builder def _apply_scope(self, scope, builder): """ Apply a single scope on the given builder instance. :param scope: The scope to apply :type scope: callable or Scope :param builder: The builder to apply the scope to :type builder: Builder """ if callable(scope): scope(builder) elif isinstance(scope, Scope): scope.apply(builder, self.get_model()) def _should_nest_wheres_for_scope(self, query, original_where_count): """ Determine if the scope added after the given offset should be nested. :type query: QueryBuilder :type original_where_count: int :rtype: bool """ return original_where_count and len(query.wheres) > original_where_count def _nest_wheres_for_scope(self, query, where_counts): """ Nest where conditions of the builder and each global scope. :type query: QueryBuilder :type where_counts: list """ # Here, we totally remove all of the where clauses since we are going to # rebuild them as nested queries by slicing the groups of wheres into # their own sections. This is to prevent any confusing logic order. wheres = query.wheres query.wheres = [] # We will take the first offset (typically 0) of where clauses and start # slicing out every scope's where clauses into their own nested where # groups for improved isolation of every scope's added constraints. previous_count = where_counts.pop(0) for where_count in where_counts: query.wheres.append( self._slice_where_conditions( wheres, previous_count, where_count - previous_count ) ) previous_count = where_count def _slice_where_conditions(self, wheres, offset, length): """ Create a where list with sliced where conditions. :type wheres: list :type offset: int :type length: int :rtype: list """ where_group = self.get_query().for_nested_where() where_group.wheres = wheres[offset:(offset + length)] return { 'type': 'nested', 'query': where_group, 'boolean': 'and' } def get_query(self): """ Get the underlying query instance. :rtype: QueryBuilder """ return self._query def to_base(self): """ Get a base query builder instance. :rtype: QueryBuilder """ return self.apply_scopes().get_query() def set_query(self, query): """ Set the underlying query instance. :param query: A QueryBuilder instance :type query: QueryBuilder """ self._query = query def get_eager_loads(self): """ Get the relationships being eager loaded. :rtype: dict """ return self._eager_load def set_eager_loads(self, eager_load): """ Sets the relationships to eager load. :type eager_load: dict :rtype: Builder """ self._eager_load = eager_load return self def get_model(self): """ Get the model instance of the model being queried :rtype: orator.Model """ return self._model def set_model(self, model): """ Set a model instance for the model being queried. :param model: The model instance :type model: orator.orm.Model :return: The current Builder instance :rtype: Builder """ self._model = model self._query.from_(model.get_table()) return self def macro(self, name, callback): """ Extend the builder with the given callback. :param name: The extension name :type name: str :param callback: The callback :type callback: callable """ self._macros[name] = callback def get_macro(self, name): """ Get the given macro by name :param name: The macro name :type name: str :return: """ return self._macros.get(name) def __dynamic(self, method): from .utils import scope scope_method = 'scope_%s' % method is_scope = False is_macro = False # New scope definition check if hasattr(self._model, method) and isinstance(getattr(self._model, method), scope): is_scope = True attribute = getattr(self._model, method) scope_method = method # Old scope definition check elif hasattr(self._model, scope_method): is_scope = True attribute = getattr(self._model, scope_method) elif method in self._macros: is_macro = True attribute = self._macros[method] else: if method in self._passthru: attribute = getattr(self.apply_scopes().get_query(), method) else: attribute = getattr(self._query, method) def call(*args, **kwargs): if is_scope: return self._call_scope(scope_method, *args, **kwargs) if is_macro: return attribute(self, *args, **kwargs) result = attribute(*args, **kwargs) if method in self._passthru: return result else: return self if not callable(attribute): return attribute return call def __getattr__(self, item, *args): return self.__dynamic(item) def __copy__(self): new = self.__class__(copy.copy(self._query)) new.set_model(self._model) return new PK!m//orator/orm/collection.py# -*- coding: utf-8 -*- from ..support.collection import Collection as BaseCollection class Collection(BaseCollection): def load(self, *relations): """ Load a set of relationships onto the collection. """ if len(self.items) > 0: query = self.first().new_query().with_(*relations) self._set_items(query.eager_load_relations(self.items)) return self def lists(self, value, key=None): """ Get a list with the values of a given key :rtype: list """ results = map(lambda x: getattr(x, value), self.items) return list(results) def model_keys(self): """ Get the list of primary keys. :rtype: list """ return map(lambda m: m.get_key(), self.items) PK!##orator/orm/factory.py# -*- coding: utf-8 -*- import os import inflection from faker import Faker from functools import wraps from .factory_builder import FactoryBuilder class Factory(object): def __init__(self, faker=None, resolver=None): """ :param faker: A faker generator instance :type faker: faker.Generator """ if faker is None: self._faker = Faker() else: self._faker = faker self._definitions = {} self._resolver = resolver @classmethod def construct(cls, faker, path_to_factories=None): """ Create a new factory container. :param faker: A faker generator instance :type faker: faker.Generator :param path_to_factories: The path to factories :type path_to_factories: str :rtype: Factory """ factory = faker.__class__() if path_to_factories is not None and os.path.isdir(path_to_factories): for filename in os.listdir(path_to_factories): if os.path.isfile(filename): cls._resolve(path_to_factories, filename) return factory def define_as(self, klass, name): """ Define a class with the given short name. :param klass: The class :type klass: class :param name: The short name :type name: str """ return self.define(klass, name) def define(self, klass, name='default'): """ Define a class with a given set of attributes. :param klass: The class :type klass: class :param name: The short name :type name: str """ def decorate(func): @wraps(func) def wrapped(*args, **kwargs): return func(*args, **kwargs) self.register(klass, func, name=name) return wrapped return decorate def register(self, klass, callback, name='default'): """ Register a class with a function. :param klass: The class :type klass: class :param callback: The callable :type callback: callable :param name: The short name :type name: str """ if klass not in self._definitions: self._definitions[klass] = {} self._definitions[klass][name] = callback def register_as(self, klass, name, callback): """ Register a class with a function. :param klass: The class :type klass: class :param callback: The callable :type callback: callable :param name: The short name :type name: str """ return self.register(klass, callback, name) def create(self, klass, **attributes): """ Create an instance of the given model and persist it to the database. :param klass: The class :type klass: class :param attributes: The instance attributes :type attributes: dict :return: mixed """ return self.of(klass).create(**attributes) def create_as(self, klass, name, **attributes): """ Create an instance of the given model and type and persist it to the database. :param klass: The class :type klass: class :param name: The type :type name: str :param attributes: The instance attributes :type attributes: dict :return: mixed """ return self.of(klass, name).create(**attributes) def make(self, klass, **attributes): """ Create an instance of the given model. :param klass: The class :type klass: class :param attributes: The instance attributes :type attributes: dict :return: mixed """ return self.of(klass).make(**attributes) def make_as(self, klass, name, **attributes): """ Create an instance of the given model and type. :param klass: The class :type klass: class :param name: The type :type name: str :param attributes: The instance attributes :type attributes: dict :return: mixed """ return self.of(klass, name).make(**attributes) def raw_of(self, klass, name, **attributes): """ Get the raw attribute dict for a given named model. :param klass: The class :type klass: class :param name: The type :type name: str :param attributes: The instance attributes :type attributes: dict :return: dict """ return self.raw(klass, _name=name, **attributes) def raw(self, klass, _name='default', **attributes): """ Get the raw attribute dict for a given named model. :param klass: The class :type klass: class :param _name: The type :type _name: str :param attributes: The instance attributes :type attributes: dict :return: dict """ raw = self._definitions[klass][_name](self._faker) raw.update(attributes) return raw def of(self, klass, name='default'): """ Create a builder for the given model. :param klass: The class :type klass: class :param name: The type :type name: str :return: orator.orm.factory_builder.FactoryBuilder """ return FactoryBuilder(klass, name, self._definitions, self._faker, self._resolver) def build(self, klass, name='default', amount=None): """ Makes a factory builder with a specified amount. :param klass: The class :type klass: class :param name: The type :type name: str :param amount: The number of models to create :type amount: int :return: mixed """ if amount is None: if isinstance(name, int): amount = name name = 'default' else: amount = 1 return self.of(klass, name).times(amount) @classmethod def _resolve(cls, path, factory_file): """ Resolve a migration instance from a file. :param path: The path to factories directory :type path: str :param factory_file: The migration file :type factory_file: str :rtype: Factory """ variables = {} name = factory_file factory_file = os.path.join(path, factory_file) with open(factory_file) as fh: exec(fh.read(), {}, variables) klass = variables[inflection.camelize(name)] instance = klass() return instance def set_connection_resolver(self, resolver): self._resolver = resolver def __getitem__(self, item): return self.make(item) def __setitem__(self, key, value): return self.define(key, value) def __contains__(self, item): return item in self._definitions def __call__(self, klass, name='default', amount=None): """ Makes a factory builder with a specified amount. :param klass: The class :type klass: class :param name: The type :type name: str :param amount: The number of models to create :type amount: int :return: mixed """ return self.build(klass, name, amount) PK!'  orator/orm/factory_builder.py# -*- coding: utf-8 -*- from .collection import Collection class FactoryBuilder(object): def __init__(self, klass, name, definitions, faker, resolver=None): """ :param klass: The class :type klass: class :param name: The type :type name: str :param definitions: The factory definitions :type definitions: dict :param faker: The faker generator instance :type faker: faker.Generator """ self._name = name self._klass = klass self._faker = faker self._definitions = definitions self._amount = 1 self._resolver = resolver def times(self, amount): """ Set the amount of models to create / make :param amount: The amount of models :type amount: int :rtype: FactoryBuilder """ self._amount = amount return self def create(self, **attributes): """ Create a collection of models and persist them to the database. :param attributes: The models attributes :type attributes: dict :return: mixed """ results = self.make(**attributes) if self._amount == 1: if self._resolver: results.set_connection_resolver(self._resolver) results.save() else: if self._resolver: results.each(lambda r: r.set_connection_resolver(self._resolver)) for result in results: result.save() return results def make(self, **attributes): """ Create a collection of models. :param attributes: The models attributes :type attributes: dict :return: mixed """ if self._amount == 1: return self._make_instance(**attributes) else: results = [] for _ in range(self._amount): results.append(self._make_instance(**attributes)) return Collection(results) def _make_instance(self, **attributes): """ Make an instance of the model with the given attributes. :param attributes: The models attributes :type attributes: dict :return: mixed """ definition = self._definitions[self._klass][self._name](self._faker) definition.update(attributes) instance = self._klass() instance.force_fill(**definition) return instance def set_connection_resolver(self, resolver): self._resolver = resolver PK!w??orator/orm/mixins/__init__.py# -*- coding: utf-8 -*- from .soft_deletes import SoftDeletes PK!wA# !orator/orm/mixins/soft_deletes.py# -*- coding: utf-8 -*- from ..scopes import SoftDeletingScope class SoftDeletes(object): __force_deleting__ = False @classmethod def boot_soft_deletes(cls, klass): """ Boot the soft deleting mixin for a model. """ klass.add_global_scope(SoftDeletingScope()) def force_delete(self): """ Force a hard delete on a soft deleted model. """ self.__force_deleting__ = True self.delete() self.__force_deleting__ = False def _perform_delete_on_model(self): """ Perform the actual delete query on this model instance. """ return self._do_perform_delete_on_model() def _do_perform_delete_on_model(self): """ Perform the actual delete query on this model instance. """ if self.__force_deleting__: return self.with_trashed().where(self.get_key_name(), self.get_key()).force_delete() return self._run_soft_delete() def _run_soft_delete(self): """ Perform the actual delete query on this model instance. """ query = self.new_query().where(self.get_key_name(), self.get_key()) time = self.fresh_timestamp() setattr(self, self.get_deleted_at_column(), time) query.update({ self.get_deleted_at_column(): self.from_datetime(time) }) def restore(self): """ Restore a soft-deleted model instance. """ if self._fire_model_event('restoring') is False: return False setattr(self, self.get_deleted_at_column(), None) self.set_exists(True) result = self.save() self._fire_model_event('restored') return result def trashed(self): """ Determine if the model instance has been soft-deleted :rtype: bool """ return getattr(self, self.get_deleted_at_column()) is not None @classmethod def with_trashed(cls): """ Get a new query builder that includes soft deletes. :rtype: orator.orm.builder.Builder """ return cls().new_query_without_scope(SoftDeletingScope()) @classmethod def only_trashed(cls): """ Get a new query builder that only includes soft deletes :type cls: orator.orm.model.Model :rtype: orator.orm.builder.Builder """ instance = cls() column = instance.get_qualified_deleted_at_column() return instance.new_query_without_scope(SoftDeletingScope()).where_not_null(column) @classmethod def restoring(cls, callback): """ Register a restoring model event with the dispatcher. :type callback: callable """ cls._register_model_event('restoring', callback) @classmethod def restored(cls, callback): """ Register a restored model event with the dispatcher. :type callback: callable """ cls._register_model_event('restored', callback) def get_deleted_at_column(self): """ Get the name of the "deleted at" column. :rtype: str """ return getattr(self, 'DELETED_AT', 'deleted_at') def get_qualified_deleted_at_column(self): """ Get the fully qualified "deleted at" column. :rtype: str """ return '%s.%s' % (self.get_table(), self.get_deleted_at_column()) PK!J""orator/orm/model.py# -*- coding: utf-8 -*- import simplejson as json import pendulum import inflection import inspect import uuid import datetime from warnings import warn from six import add_metaclass from collections import OrderedDict from ..utils import basestring, deprecated from ..exceptions.orm import MassAssignmentError, RelatedClassNotFound from ..query import QueryBuilder from .builder import Builder from .collection import Collection from .relations import ( Relation, HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphOne, MorphMany, MorphTo, MorphToMany ) from .relations.wrapper import Wrapper, BelongsToManyWrapper from .utils import mutator, accessor from .scopes import Scope from ..events import Event class ModelRegister(dict): def __init__(self, *args, **kwargs): self.inverse = {} super(ModelRegister, self).__init__(*args, **kwargs) def __setitem__(self, key, value): super(ModelRegister, self).__setitem__(key, value) self.inverse[value] = key def __delitem__(self, key): del self.inverse[self[key]] super(ModelRegister, self).__delitem__(key) class MetaModel(type): __register__ = {} def __init__(cls, *args, **kwargs): name = cls.__table__ or inflection.tableize(cls.__name__) cls._register[name] = cls super(MetaModel, cls).__init__(*args, **kwargs) def __getattr__(cls, item): try: return type.__getattribute__(cls, item) except AttributeError: query = cls.query() return getattr(query, item) @add_metaclass(MetaModel) class Model(object): __connection__ = None __table__ = None __primary_key__ = 'id' __incrementing__ = True __fillable__ = [] __guarded__ = ['*'] __unguarded__ = False __hidden__ = [] __visible__ = [] __appends__ = [] __timestamps__ = True __dates__ = [] __casts__ = {} __touches__ = [] __morph_name__ = None _per_page = 15 _with = [] _booted = {} _global_scopes = {} _registered = [] _accessor_cache = {} _mutator_cache = {} __resolver = None __columns__ = [] __dispatcher__ = Event() __observables__ = [] _register = ModelRegister() __attributes__ = {} many_methods = ['belongs_to_many', 'morph_to_many', 'morphed_by_many'] CREATED_AT = 'created_at' UPDATED_AT = 'updated_at' def __init__(self, _attributes=None, **attributes): """ :param attributes: The instance attributes """ self._boot_if_not_booted() self._exists = False self._original = {} # Setting default attributes' values self._attributes = dict((k, v) for k, v in self.__attributes__.items()) self._relations = {} self.sync_original() if _attributes is not None: attributes.update(_attributes) self.fill(**attributes) def _boot_if_not_booted(self): """ Check if the model needs to be booted and if so, do it. """ klass = self.__class__ if not klass._booted.get(klass): klass._booted[klass] = True self._fire_model_event('booting') klass._boot() self._fire_model_event('booted') @classmethod def _boot(cls): """ The booting method of the model. """ cls._accessor_cache[cls] = {} cls._mutator_cache[cls] = {} for name, method in cls.__dict__.items(): if isinstance(method, accessor): cls._accessor_cache[cls][method.attribute] = method elif isinstance(method, mutator): cls._mutator_cache[cls][method.attribute] = method cls._boot_mixins() @classmethod def _boot_columns(cls): connection = cls.resolve_connection() columns = connection.get_schema_manager().list_table_columns(cls.__table__ or inflection.tableize(cls.__name__)) cls.__columns__ = list(columns.keys()) @classmethod def _boot_mixins(cls): """ Boot the mixins """ for mixin in cls.__bases__: #if mixin == Model: # continue method = 'boot_%s' % inflection.underscore(mixin.__name__) if hasattr(mixin, method): getattr(mixin, method)(cls) @classmethod def add_global_scope(cls, scope, implementation = None): """ Register a new global scope on the model. :param scope: The scope to register :type scope: orator.orm.scopes.scope.Scope or callable or str :param implementation: The scope implementation :type implementation: callbale or None """ if cls not in cls._global_scopes: cls._global_scopes[cls] = OrderedDict() if isinstance(scope, basestring) and implementation is not None: cls._global_scopes[cls][scope] = implementation elif callable(scope): cls._global_scopes[cls][uuid.uuid4().hex] = scope elif isinstance(scope, Scope): cls._global_scopes[cls][scope.__class__] = scope else: raise Exception('Global scope must be an instance of Scope or a callable') @classmethod def has_global_scope(cls, scope): """ Determine if a model has a global scope. :param scope: The scope to register :type scope: orator.orm.scopes.scope.Scope or str """ return cls.get_global_scope(scope) is not None @classmethod def get_global_scope(cls, scope): """ Get a global scope registered with the model. :param scope: The scope to register :type scope: orator.orm.scopes.scope.Scope or str """ for key, value in cls._global_scopes[cls].items(): if isinstance(scope, key): return value def get_global_scopes(self): """ Get the global scopes for this class instance. :rtype: dict """ return self.__class__._global_scopes.get(self.__class__, {}) @classmethod def observe(cls, observer): """ Register an observer with the Model. :param observer: The observer """ for event in cls.get_observable_events(): if hasattr(observer, event): cls._register_model_event(event, getattr(observer, event)) def fill(self, _attributes=None, **attributes): """ Fill the model with attributes. :param attributes: The instance attributes :type attributes: dict :return: The model instance :rtype: Model :raises: MassAssignmentError """ if _attributes is not None: attributes.update(_attributes) totally_guarded = self.totally_guarded() for key, value in self._fillable_from_dict(attributes).items(): key = self._remove_table_from_key(key) if self.is_fillable(key): self.set_attribute(key, value) elif totally_guarded: raise MassAssignmentError(key) return self def force_fill(self, _attributes=None, **attributes): """ Fill the model with attributes. Force mass assignment. :param attributes: The instance attributes :type attributes: dict :return: The model instance :rtype: Model """ if _attributes is not None: attributes.update(_attributes) self.unguard() self.fill(**attributes) self.reguard() return self def _fillable_from_dict(self, attributes): """ Get the fillable attributes from a given dictionary. :type attributes: dict :return: The fillable attributes :rtype: dict """ if self.__fillable__ and not self.__unguarded__: return {x: attributes[x] for x in attributes if x in self.__fillable__} return attributes def new_instance(self, attributes=None, exists=False): """ Create a new instance for the given model. :param attributes: The instance attributes :type attributes: dict :param exists: :type exists: bool :return: A new instance for the current model :rtype: Model """ if attributes is None: attributes = {} model = self.__class__(**attributes) model.set_connection(self.get_connection_name()) model.set_exists(exists) return model def new_from_builder(self, attributes=None, connection=None): """ Create a new model instance that is existing. :param attributes: The model attributes :type attributes: dict :param connection: The connection name :type connection: str :return: A new instance for the current model :rtype: Model """ model = self.new_instance({}, True) if attributes is None: attributes = {} model.set_raw_attributes(attributes, True) model.set_connection(connection or self.__connection__) return model @classmethod def hydrate(cls, items, connection=None): """ Create a collection of models from plain lists. :param items: :param connection: :return: """ instance = cls().set_connection(connection) collection = instance.new_collection(items) return collection.map(lambda item: instance.new_from_builder(item)) @classmethod def hydrate_raw(cls, query, bindings=None, connection=None): """ Create a collection of models from a raw query. :param query: The SQL query :type query: str :param bindings: The query bindings :type bindings: list :param connection: The connection name :rtype: Collection """ instance = cls().set_connection(connection) items = instance.get_connection().select(query, bindings) return cls.hydrate(items, connection) @classmethod def create(cls, _attributes=None, **attributes): """ Save a new model an return the instance. :param attributes: The instance attributes :type attributes: dict :return: The new instance :rtype: Model """ if _attributes is not None: attributes.update(_attributes) model = cls(**attributes) model.save() return model @classmethod def force_create(cls, **attributes): """ Save a new model an return the instance. Allow mass assignment. :param attributes: The instance attributes :type attributes: dict :return: The new instance :rtype: Model """ cls.unguard() model = cls.create(**attributes) cls.reguard() return model @classmethod def first_or_create(cls, **attributes): """ Get the first record matching the attributes or create it. :param attributes: The instance attributes :type attributes: dict :return: The new instance :rtype: Model """ instance = cls().new_query_without_scopes().where(attributes).first() if instance is not None: return instance return cls.create(**attributes) @classmethod def first_or_new(cls, **attributes): """ Get the first record matching the attributes or instantiate it. :param attributes: The instance attributes :type attributes: dict :return: The new instance :rtype: Model """ instance = cls().new_query_without_scopes().where(attributes).first() if instance is not None: return instance return cls(**attributes) @classmethod def update_or_create(cls, attributes, values=None): """ Create or update a record matching the attributes, and fill it with values. :param attributes: The instance attributes :type attributes: dict :param values: The values :type values: dict :return: The new instance :rtype: Model """ instance = cls.first_or_new(**attributes) if values is None: values = {} instance.fill(**values).save() return instance @classmethod def query(cls): """ Begin querying the model. :return: A Builder instance :rtype: orator.orm.Builder """ return cls().new_query() @classmethod def on(cls, connection=None): """ Begin querying the model on a given connection. :param connection: The connection name :type connection: str :return: A Builder instance :rtype: orator.orm.Builder """ instance = cls() instance.set_connection(connection) return instance.new_query() @classmethod def on_write_connection(cls): """ Begin querying the model on the write connection. :return: A Builder instance :rtype: QueryBuilder """ instance = cls() return instance.new_query().use_write_connection() @classmethod def all(cls, columns=None): """ Get all og the models from the database. :param columns: The columns to retrieve :type columns: list :return: A Collection instance :rtype: Collection """ instance = cls() return instance.new_query().get(columns) @classmethod def find(cls, id, columns=None): """ Find a model by its primary key. :param id: The id of the model :type id: mixed :param columns: The columns to retrieve :type columns: list :return: Either a Model instance or a Collection :rtype: Model """ instance = cls() if isinstance(id, list) and not id: return instance.new_collection() if columns is None: columns = ['*'] return instance.new_query().find(id, columns) @classmethod def find_or_new(cls, id, columns=None): """ Find a model by its primary key or return new instance. :param id: The id of the model :type id: mixed :param columns: The columns to retrieve :type columns: list :return: A Model instance :rtype: Model """ instance = cls.find(id, columns) if instance is not None: return instance return cls() def fresh(self, with_=None): """ Reload a fresh instance from the database. :param with_: The list of relations to eager load :type with_: list :return: The current model instance :rtype: Model """ if with_ is None: with_ = () key = self.get_key_name() if self.exists: return self.with_(*with_).where(key, self.get_key()).first() def load(self, *relations): """ Eager load relations on the model :param relations: The relations to eager load :type relations: str or list :return: The current model instance :rtype: Model """ query = self.new_query().with_(*relations) query.eager_load_relations([self]) return self @classmethod def with_(cls, *relations): """ Begin querying a model with eager loading :param relations: The relations to eager load :type relations: str or list :return: A Builder instance :rtype: Builder """ instance = cls() return instance.new_query().with_(*relations) def has_one(self, related, foreign_key=None, local_key=None, relation=None, _wrapped=True): """ Define a one to one relationship. :param related: The related model: :type related: Model or str :param foreign_key: The foreign key :type foreign_key: str :param local_key: The local key :type local_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: HasOne """ if relation is None: name = inspect.stack()[1][3] else: name = relation if name in self._relations: return self._relations[name] if not foreign_key: foreign_key = self.get_foreign_key() instance = self._get_related(related, True) if not local_key: local_key = self.get_key_name() rel = HasOne(instance.new_query(), self, '%s.%s' % (instance.get_table(), foreign_key), local_key) if _wrapped: warn('Using has_one method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[name] = rel return rel def morph_one(self, related, name, type_column=None, id_column=None, local_key=None, relation=None, _wrapped=True): """ Define a polymorphic one to one relationship. :param related: The related model: :type related: Model or str :param type_column: The name of the type column :type type_column: str :param id_column: The name of the id column :type id_column: str :param local_key: The local key :type local_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: HasOne """ if relation is None: relation = inspect.stack()[1][3] if relation in self._relations: return self._relations[name] instance = self._get_related(related, True) type_column, id_column = self.get_morphs(name, type_column, id_column) table = instance.get_table() if not local_key: local_key = self.get_key_name() rel = MorphOne(instance.new_query(), self, '%s.%s' % (table, type_column), '%s.%s' % (table, id_column), local_key) if _wrapped: warn('Using morph_one method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[relation] = rel return rel def belongs_to(self, related, foreign_key=None, other_key=None, relation=None, _wrapped=True): """ Define an inverse one to one or many relationship. :param related: The related model: :type related: Model or str :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :type relation: str :rtype: BelongsTo """ if relation is None: relation = inspect.stack()[1][3] if relation in self._relations: return self._relations[relation] if foreign_key is None: foreign_key = '%s_id' % inflection.underscore(relation) instance = self._get_related(related, True) query = instance.new_query() if not other_key: other_key = instance.get_key_name() rel = BelongsTo(query, self, foreign_key, other_key, relation) if _wrapped: warn('Using belongs_to method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[relation] = rel return rel def morph_to(self, name=None, type_column=None, id_column=None, _wrapped=True): """ Define a polymorphic, inverse one-to-one or many relationship. :param name: The name of the relation :type name: str :param type_column: The type column :type type_column: str :param id_column: The id column :type id_column: str :rtype: MorphTo """ if not name: name = inspect.stack()[1][3] if name in self._relations: return self._relations[name] type_column, id_column = self.get_morphs(name, type_column, id_column) # If the type value is null it is probably safe to assume we're eager loading # the relationship. When that is the case we will pass in a dummy query as # there are multiple types in the morph and we can't use single queries. if not hasattr(self, type_column): return MorphTo(self.new_query(), self, id_column, None, type_column, name) # If we are not eager loading the relationship we will essentially treat this # as a belongs-to style relationship since morph-to extends that class and # we will pass in the appropriate values so that it behaves as expected. klass = self.get_actual_class_for_morph(getattr(self, type_column)) instance = klass() instance.set_connection(self.get_connection_name()) rel = MorphTo(instance.new_query(), self, id_column, instance.get_key_name(), type_column, name) if _wrapped: warn('Using morph_to method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[name] = rel return rel def get_actual_class_for_morph(self, slug): """ Retrieve the class from a slug. :param slug: The slug :type slug: str """ for cls in self.__class__._register.values(): morph_name = cls.get_morph_name() if morph_name == slug: return cls def has_many(self, related, foreign_key=None, local_key=None, relation=None, _wrapped=True): """ Define a one to many relationship. :param related: The related model :type related: Model or str :param foreign_key: The foreign key :type foreign_key: str :param local_key: The local key :type local_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: HasOne """ if relation is None: name = inspect.stack()[1][3] else: name = relation if name in self._relations: return self._relations[name] if not foreign_key: foreign_key = self.get_foreign_key() instance = self._get_related(related, True) if not local_key: local_key = self.get_key_name() rel = HasMany(instance.new_query(), self, '%s.%s' % (instance.get_table(), foreign_key), local_key) if _wrapped: warn('Using has_many method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[name] = rel return rel def has_many_through(self, related, through, first_key=None, second_key=None, relation=None, _wrapped=True): """ Define a has-many-through relationship. :param related: The related model :type related: Model or str :param through: The through model :type through: Model or str :param first_key: The first key :type first_key: str :param second_key: The second_key :type second_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: HasManyThrough """ if relation is None: name = inspect.stack()[1][3] else: name = relation if name in self._relations: return self._relations[name] through = self._get_related(through, True) if not first_key: first_key = self.get_foreign_key() if not second_key: second_key = through.get_foreign_key() rel = HasManyThrough(self._get_related(related)().new_query(), self, through, first_key, second_key) if _wrapped: warn('Using has_many_through method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[name] = rel return rel def morph_many(self, related, name, type_column=None, id_column=None, local_key=None, relation=None, _wrapped=True): """ Define a polymorphic one to many relationship. :param related: The related model: :type related: Model or str :param type_column: The name of the type column :type type_column: str :param id_column: The name of the id column :type id_column: str :param local_key: The local key :type local_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: MorphMany """ if relation is None: relation = inspect.stack()[1][3] if relation in self._relations: return self._relations[relation] instance = self._get_related(related, True) type_column, id_column = self.get_morphs(name, type_column, id_column) table = instance.get_table() if not local_key: local_key = self.get_key_name() rel = MorphMany(instance.new_query(), self, '%s.%s' % (table, type_column), '%s.%s' % (table, id_column), local_key) if _wrapped: warn('Using morph_many method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[name] = rel return rel def belongs_to_many(self, related, table=None, foreign_key=None, other_key=None, relation=None, _wrapped=True): """ Define a many-to-many relationship. :param related: The related model :type related: Model or str :param table: The pivot table :type table: str :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :type relation: str :rtype: BelongsToMany """ if relation is None: relation = inspect.stack()[1][3] if relation in self._relations: return self._relations[relation] if not foreign_key: foreign_key = self.get_foreign_key() instance = self._get_related(related, True) if not other_key: other_key = instance.get_foreign_key() if table is None: table = self.joining_table(instance) query = instance.new_query() rel = BelongsToMany(query, self, table, foreign_key, other_key, relation) if _wrapped: warn('Using belongs_to_many method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = BelongsToManyWrapper(rel) self._relations[relation] = rel return rel def morph_to_many(self, related, name, table=None, foreign_key=None, other_key=None, inverse=False, relation=None, _wrapped=True): """ Define a polymorphic many-to-many relationship. :param related: The related model: :type related: Model or str :param name: The relation name :type name: str :param table: The pivot table :type table: str :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: MorphToMany """ if relation is None: caller = inspect.stack()[1][3] else: caller = relation if caller in self._relations: return self._relations[caller] if not foreign_key: foreign_key = name + '_id' instance = self._get_related(related, True) if not other_key: other_key = instance.get_foreign_key() query = instance.new_query() if not table: table = inflection.pluralize(name) rel = MorphToMany(query, self, name, table, foreign_key, other_key, caller, inverse) if _wrapped: warn('Using morph_to_many method directly is deprecated. ' 'Use the appropriate decorator instead.', category=DeprecationWarning) rel = Wrapper(rel) self._relations[caller] = rel return rel def morphed_by_many(self, related, name, table=None, foreign_key=None, other_key=None, relation=None, _wrapped=False): """ Define a polymorphic many-to-many relationship. :param related: The related model: :type related: Model or str :param name: The relation name :type name: str :param table: The pivot table :type table: str :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :param relation: The name of the relation (defaults to method name) :type relation: str :rtype: MorphToMany """ if not foreign_key: foreign_key = self.get_foreign_key() if not other_key: other_key = name + '_id' return self.morph_to_many(related, name, table, foreign_key, other_key, True, relation, _wrapped) def _get_related(self, related, as_instance=False): """ Get the related class. :param related: The related model or table :type related: Model or str :rtype: Model class """ if not isinstance(related, basestring) and issubclass(related, Model): if as_instance: instance = related() instance.set_connection(self.get_connection_name()) return instance return related related_class = self.__class__._register.get(related) if related_class: if as_instance: instance = related_class() instance.set_connection(self.get_connection_name()) return instance return related_class raise RelatedClassNotFound(related) def joining_table(self, related): """ Get the joining table name for a many-to-many relation :param related: The related model :type related: Model :rtype: str """ base = self.get_table() related = related.get_table() models = sorted([related, base]) return '_'.join(models) @classmethod def destroy(cls, *ids): """ Destroy the models for the given IDs :param ids: The ids of the models to destroy :type ids: tuple :return: The number of models destroyed :rtype: int """ count = 0 if len(ids) == 1 and isinstance(ids[0], list): ids = ids[0] ids = list(ids) instance = cls() key = instance.get_key_name() for model in instance.new_query().where_in(key, ids).get(): if model.delete(): count += 1 return count def delete(self): """ Delete the model from the database. :rtype: bool or None :raises: Exception """ if self.__primary_key__ is None: raise Exception('No primary key defined on the model.') if self._exists: if self._fire_model_event('deleting') is False: return False self.touch_owners() self._perform_delete_on_model() self._exists = False self._fire_model_event('deleted') return True def force_delete(self): """ Force a hard delete on a soft deleted model. """ return self.delete() def _perform_delete_on_model(self): """ Perform the actual delete query on this model instance. """ if hasattr(self, '_do_perform_delete_on_model'): return self._do_perform_delete_on_model() return self.new_query().where(self.get_key_name(), self.get_key()).delete() @classmethod def saving(cls, callback): """ Register a saving model event with the dispatcher. :type callback: callable """ cls._register_model_event('saving', callback) @classmethod def saved(cls, callback): """ Register a saved model event with the dispatcher. :type callback: callable """ cls._register_model_event('saved', callback) @classmethod def updating(cls, callback): """ Register a updating model event with the dispatcher. :type callback: callable """ cls._register_model_event('updating', callback) @classmethod def updated(cls, callback): """ Register a updated model event with the dispatcher. :type callback: callable """ cls._register_model_event('updated', callback) @classmethod def creating(cls, callback): """ Register a creating model event with the dispatcher. :type callback: callable """ cls._register_model_event('creating', callback) @classmethod def created(cls, callback): """ Register a created model event with the dispatcher. :type callback: callable """ cls._register_model_event('created', callback) @classmethod def deleting(cls, callback): """ Register a deleting model event with the dispatcher. :type callback: callable """ cls._register_model_event('deleting', callback) @classmethod def deleted(cls, callback): """ Register a deleted model event with the dispatcher. :type callback: callable """ cls._register_model_event('deleted', callback) @classmethod def flush_event_listeners(cls): """ Remove all of the event listeners for the model. """ if not cls.__dispatcher__: return for event in cls.get_observable_events(): cls.__dispatcher__.forget('%s: %s' % (event, cls.__name__)) @classmethod def _register_model_event(cls, event, callback): """ Register a model event with the dispatcher. :param event: The event :type event: str :param callback: The callback :type callback: callable """ if cls.__dispatcher__: cls.__dispatcher__.listen('%s: %s' % (event, cls.__name__), callback) @classmethod def get_observable_events(cls): """ Get the observable event names. :rtype: list """ default_events = [ 'creating', 'created', 'updating', 'updated', 'deleting', 'deleted', 'saving', 'saved', 'restoring', 'restored' ] return default_events + cls.__observables__ def _increment(self, column, amount=1): """ Increment a column's value :param column: The column to increment :type column: str :param amount: The amount by which to increment :type amount: int :return: The new column value :rtype: int """ return self._increment_or_decrement(column, amount, 'increment') def _decrement(self, column, amount=1): """ Decrement a column's value :param column: The column to increment :type column: str :param amount: The amount by which to increment :type amount: int :return: The new column value :rtype: int """ return self._increment_or_decrement(column, amount, 'decrement') def _increment_or_decrement(self, column, amount, method): """ Runthe increment or decrement method on the model :param column: The column to increment or decrement :type column: str :param amount: The amount by which to increment or decrement :type amount: int :param method: The method :type method: str :return: The new column value :rtype: int """ query = self.new_query() if not self._exists: return getattr(query, method)(column, amount) self._increment_or_decrement_attribute_value(column, amount, method) query = query.where(self.get_key_name(), self.get_key()) return getattr(query, method)(column, amount) def _increment_or_decrement_attribute_value(self, column, amount, method): """ Increment the underlying attribute value and sync with original. :param column: The column to increment or decrement :type column: str :param amount: The amount by which to increment or decrement :type amount: int :param method: The method :type method: str :return: None """ setattr(self, column, getattr(self, column) + (amount if method == 'increment' else amount * -1)) self.sync_original_attribute(column) def update(self, _attributes=None, **attributes): """ Update the model in the database. :param attributes: The model attributes :type attributes: dict :return: The number of rows affected :rtype: int """ if _attributes is not None: attributes.update(_attributes) if not self._exists: return self.new_query().update(**attributes) return self.fill(**attributes).save() def push(self): """ Save the model and all of its relationship. """ if not self.save(): return False for models in self._relations.values(): if isinstance(models, Collection): models = models.all() else: models = [models] for model in models: if not model: continue if not model.push(): return False return True def save(self, options=None): """ Save the model to the database. """ if options is None: options = {} query = self.new_query() if self._fire_model_event('saving') is False: return False if self._exists: saved = self._perform_update(query, options) else: saved = self._perform_insert(query, options) if saved: self._finish_save(options) return saved def _finish_save(self, options): """ Finish processing on a successful save operation. """ self._fire_model_event('saved') self.sync_original() if options.get('touch', True): self.touch_owners() def _perform_update(self, query, options=None): """ Perform a model update operation. :param query: A Builder instance :type query: Builder :param options: Extra options :type options: dict """ if options is None: options = {} dirty = self.get_dirty() if len(dirty): if self._fire_model_event('updating') is False: return False if self.__timestamps__ and options.get('timestamps', True): self._update_timestamps() dirty = self.get_dirty() if len(dirty): self._set_keys_for_save_query(query).update(dirty) self._fire_model_event('updated') return True def _perform_insert(self, query, options=None): """ Perform a model update operation. :param query: A Builder instance :type query: Builder :param options: Extra options :type options: dict """ if options is None: options = {} if self._fire_model_event('creating') is False: return False if self.__timestamps__ and options.get('timestamps', True): self._update_timestamps() attributes = self._attributes if self.__incrementing__: self._insert_and_set_id(query, attributes) else: query.insert(attributes) self._exists = True self._fire_model_event('created') return True def _insert_and_set_id(self, query, attributes): """ Insert the given attributes and set the ID on the model. :param query: A Builder instance :type query: Builder :param attributes: The attributes to insert :type attributes: dict """ key_name = self.get_key_name() id = query.insert_get_id(attributes, key_name) self.set_attribute(key_name, id) def touch_owners(self): """ Touch the owning relations of the model. """ for relation in self.__touches__: if hasattr(self, relation): _relation = getattr(self, relation) if _relation: _relation.touch() _relation.touch_owners() def touches(self, relation): """ Determine if a model touches a given relation. :param relation: The relation to check. :type relation: str :rtype: bool """ return relation in self.__touches__ def _fire_model_event(self, event): """ Fire the given event for the model. :type event: str """ if not self.__dispatcher__: return True # We will append the names of the class to the event to distinguish it from # other model events that are fired, allowing us to listen on each model # event set individually instead of catching event for all the models. event = '%s: %s' % (event, self.__class__.__name__) return self.__dispatcher__.fire(event, self) def _set_keys_for_save_query(self, query): """ Set the keys for a save update query. :param query: A Builder instance :type query: Builder :return: The Builder instance :rtype: Builder """ query.where(self.get_key_name(), self._get_key_for_save_query()) return query def _get_key_for_save_query(self): """ Get the primary key value for a save query. """ if self.get_key_name() in self._original: return self._original[self.get_key_name()] return self._attributes[self.get_key_name()] def touch(self): """ Update the model's timestamps. :rtype: bool """ if not self.__timestamps__: return False self._update_timestamps() return self.save() def _update_timestamps(self): """ Update the model's timestamps. """ time = self.fresh_timestamp() if not self.is_dirty(self.UPDATED_AT) and self._should_set_timestamp(self.UPDATED_AT): self.set_updated_at(time) if not self._exists and not self.is_dirty(self.CREATED_AT) and self._should_set_timestamp(self.CREATED_AT): self.set_created_at(time) def _should_set_timestamp(self, timestamp): """ Determine if a timestamp should be set. :param timestamp: The timestamp to check :type timestamp: str :rtype: bool """ if isinstance(self.__timestamps__, bool): return self.__timestamps__ return timestamp in self.__timestamps__ def set_created_at(self, value): """ Set the value of the "created at" attribute. :param value: The value :type value: datetime """ self.set_attribute(self.CREATED_AT, value) def set_updated_at(self, value): """ Set the value of the "updated at" attribute. :param value: The value :type value: datetime """ self.set_attribute(self.UPDATED_AT, value) def get_created_at_column(self): """ Get the name of the "created at" column. :rtype: str """ return self.CREATED_AT def get_updated_at_column(self): """ Get the name of the "updated at" column. :rtype: str """ return self.UPDATED_AT def fresh_timestamp(self): """ Get a fresh timestamp for the model. :return: pendulum.Pendulum """ return pendulum.utcnow() def fresh_timestamp_string(self): """ Get a fresh timestamp string for the model. :return: str """ return self.from_datetime(self.fresh_timestamp()) def new_query(self): """ Get a new query builder for the model's table :return: A Builder instance :rtype: Builder """ builder = self.new_query_without_scopes() for identifier, scope in self.get_global_scopes().items(): builder.with_global_scope(identifier, scope) return builder def new_query_without_scope(self, scope): """ Get a new query builder for the model's table without a given scope :return: A Builder instance :rtype: Builder """ builder = self.new_query() return builder.without_global_scope(scope) def new_query_without_scopes(self): """ Get a new query builder without any scopes. :return: A Builder instance :rtype: Builder """ builder = self.new_orm_builder( self._new_base_query_builder() ) return builder.set_model(self).with_(*self._with) @classmethod def query(cls): return cls().new_query() def new_orm_builder(self, query): """ Create a new orm query builder for the model :param query: A QueryBuilder instance :type query: QueryBuilder :return: A Builder instance :rtype: Builder """ return Builder(query) def _new_base_query_builder(self): """ Get a new query builder instance for the connection. :return: A QueryBuilder instance :rtype: QueryBuilder """ conn = self.get_connection() return conn.query() def new_collection(self, models=None): """ Create a new Collection instance. :param models: A list of models :type models: list :return: A new Collection instance :rtype: Collection """ if models is None: models = [] return Collection(models) def new_pivot(self, parent, attributes, table, exists): """ Create a new pivot model instance. :param parent: The parent model :type parent: Model :param attributes: The pivot attributes :type attributes: dict :param table: the pivot table :type table: str :param exists: Whether the pivot exists or not :type exists: bool :rtype: Pivot """ from .relations.pivot import Pivot return Pivot(parent, attributes, table, exists) def get_table(self): """ Get the table associated with the model. :return: The name of the table :rtype: str """ return self.__class__._register.inverse[self.__class__] def set_table(self, table): """ Set the table associated with the model. :param table: The table name :type table: str """ old_table = self.__class__._register.inverse.get(self.__class__, None) self.__table__ = table if old_table: del self.__class__._register[old_table] self.__class__._register[self.__table__] = self.__class__ def get_key(self): """ Get the value of the model's primary key. """ return self.get_attribute(self.get_key_name()) def get_key_name(self): """ Get the primary key for the model. :return: The primary key name :rtype: str """ return self.__primary_key__ def set_key_name(self, name): """ Set the primary key for the model. :param name: The primary key name :type name: str """ self.__primary_key__ = name def get_qualified_key_name(self): """ Get the table qualified key name. :rtype: str """ return '%s.%s' % (self.get_table(), self.get_key_name()) def uses_timestamps(self): """ Determine if the model uses timestamps. :rtype: bool """ return self.__timestamps__ def get_morphs(self, name, type, id): """ Get the polymorphic relationship columns. """ if not type: type = name + '_type' if not id: id = name + '_id' return type, id @classmethod def get_morph_name(cls): """ Get the name for polymorphic relations. """ if not cls.__morph_name__: return cls._register.inverse[cls] return cls.__morph_name__ def get_per_page(self): """ Get the number of models to return per page. :rtype: int """ return self._per_page def get_foreign_key(self): """ Get the default foreign key name for the model :rtype: str """ return '%s_id' % inflection.singularize(self.get_table()) def get_hidden(self): """ Get the hidden attributes for the model. """ return self.__hidden__ def set_hidden(self, hidden): """ Set the hidden attributes for the model. :param hidden: The attributes to add :type hidden: list """ self.__hidden__ = hidden return self def add_hidden(self, *attributes): """ Add hidden attributes to the model. :param attributes: The attributes to hide :type attributes: list """ self.__hidden__ += attributes def get_visible(self): """ Get the visible attributes for the model. """ return self.__visible__ def set_visible(self, visible): """ Set the visible attributes for the model. :param visible: The attributes to make visible :type visible: list """ self.__visible__ = visible return self def add_visible(self, *attributes): """ Add visible attributes to the model. :param attributes: The attributes to make visible :type attributes: list """ self.__visible__ += attributes def get_fillable(self): """ Get the fillable attributes for the model. :rtype: list """ return self.__fillable__ def fillable(self, fillable): """ Set the fillable attributes for the model. :param fillable: The fillable attributes :type fillable: list :return: The current Model instance :rtype: Model """ self.__fillable__ = fillable return self def get_guarded(self): """ Get the guarded attributes. """ return self.__guarded__ def guard(self, guarded): """ Set the guarded attributes. :param guarded: The guarded attributes :type guarded: list :return: The current Model instance :rtype: Model """ self.__guarded__ = guarded return self @classmethod def unguard(cls): """ Disable the mass assigment restrictions. """ cls.__unguarded__ = True @classmethod def reguard(cls): """ Enable the mass assignment restrictions. :return: """ cls.__unguarded__ = False def is_fillable(self, key): """ Determine if the given attribute can be mass assigned. :param key: The attribute to check :type key: str :return: Whether the attribute can be mass assigned or not :rtype: bool """ if self.__unguarded__: return True if key in self.__fillable__: return True if self.is_guarded(key): return False return not self.__fillable__ and not key.startswith('_') def is_guarded(self, key): """ Determine if the given attribute is guarded. :param key: The attribute to check :type key: str :return: Whether the attribute is guarded or not :rtype: bool """ return key in self.__guarded__ or self.__guarded__ == ['*'] def totally_guarded(self): """ Determine if the model is totally guarded. :rtype: bool """ return len(self.__fillable__) == 0 and self.__guarded__ == ['*'] def _remove_table_from_key(self, key): """ Remove the table name from a given key. :param key: The key to remove the table name from. :type key: str :rtype: str """ if '.' not in key: return key return key.split('.')[-1] def get_incrementing(self): return self.__incrementing__ def set_incrementing(self, value): self.__incrementing__ = value def to_json(self, **options): """ Convert the model instance to JSON. :param options: The JSON options :type options: dict :return: The JSON encoded model instance :rtype: str """ return json.dumps(self.to_dict(), **options) def serialize(self): """ Convert the model instance to a dictionary. :return: The dictionary version of the model instance :rtype: dict """ attributes = self.attributes_to_dict() attributes.update(self.relations_to_dict()) return attributes @deprecated def to_dict(self): """ Convert the model instance to a dictionary. :return: The dictionary version of the model instance :rtype: dict """ return self.serialize() def attributes_to_dict(self): """ Convert the model's attributes to a dictionary. :rtype: dict """ attributes = self._get_dictable_attributes() mutated_attributes = self._get_mutated_attributes() for key in self.get_dates(): if not key in attributes or key in mutated_attributes: continue attributes[key] = self._format_date(attributes[key]) for key in mutated_attributes: if key not in attributes: continue attributes[key] = self._mutate_attribute_for_dict(key) # Next we will handle any casts that have been setup for this model and cast # the values to their appropriate type. If the attribute has a mutator we # will not perform the cast on those attributes to avoid any confusion. for key, value in self.__casts__.items(): if key not in attributes or key in mutated_attributes: continue attributes[key] = self._cast_attribute(key, attributes[key]) # Here we will grab all of the appended, calculated attributes to this model # as these attributes are not really in the attributes array, but are run # when we need to array or JSON the model for convenience to the coder. for key in self._get_dictable_appends(): attributes[key] = self._mutate_attribute_for_dict(key) return attributes def _get_dictable_attributes(self): """ Get an attribute dictionary of all dictable attributes. :rtype: dict """ return self._get_dictable_items(self._attributes) def _get_dictable_appends(self): """ Get all the appendable values that are dictable. :rtype: list """ if not self.__appends__: return [] return self._get_dictable_items(dict(zip(self.__appends__, self.__appends__))) def relations_to_dict(self): """ Get the model's relationships in dictionary form. :rtype: dict """ attributes = {} for key, value in self._get_dictable_relations().items(): if key in self.get_hidden(): continue relation = None if hasattr(value, 'serialize'): relation = value.serialize() elif hasattr(value, 'to_dict'): relation = value.to_dict() elif value is None: relation = value if relation is not None or value is None: attributes[key] = relation return attributes def _get_dictable_relations(self): """ Get an attribute dict of all dictable relations. """ return self._get_dictable_items(self._relations) def _get_dictable_items(self, values): """ Get an attribute dictionary of all dictable values. :param values: The values to check :type values: dict :rtype: dict """ if len(self.__visible__) > 0: return {x: values[x] for x in values.keys() if x in self.__visible__} return {x: values[x] for x in values.keys() if x not in self.__hidden__ and not x.startswith('_')} def get_attribute(self, key, original=None): """ Get an attribute from the model. :param key: The attribute to get :type key: str """ in_attributes = key in self._attributes if in_attributes: return self._get_attribute_value(key) if key in self._relations: return self._relations[key] relation = original or super(Model, self).__getattribute__(key) if relation: return self._get_relationship_from_method(key, relation) raise AttributeError(key) def get_raw_attribute(self, key): """ Get the raw underlying attribute. :param key: The attribute to get :type key: str """ return self._attributes[key] def _get_attribute_value(self, key): """ Get a plain attribute. :param key: The attribute to get :type key: str """ value = self._get_attribute_from_dict(key) if self._has_cast(key): value = self._cast_attribute(key, value) elif key in self.get_dates(): if value is not None: return self.as_datetime(value) return value def _get_attribute_from_dict(self, key): return self._attributes.get(key) def _get_relationship_from_method(self, method, relations=None): """ Get a relationship value from a method. :param method: The method name :type method: str :rtype: mixed """ relations = relations or super(Model, self).__getattribute__(method) if not isinstance(relations, Relation): raise RuntimeError('Relationship method must return an object of type Relation') self._relations[method] = relations return self._relations[method] def has_get_mutator(self, key): """ Determine if a get mutator exists for an attribute. :param key: The attribute name :type key: str :rtype: bool """ return hasattr(self, 'get_%s_attribute' % inflection.underscore(key)) def _mutate_attribute_for_dict(self, key): """ Get the value of an attribute using its mutator for dict conversion. :param key: The attribute name :type key: str """ value = getattr(self, key) if hasattr(value, 'to_dict'): return value.to_dict() if key in self.get_dates(): return self._format_date(value) return value def _has_cast(self, key): """ Determine whether an attribute should be casted to a native type. :param key: The attribute to check :type key: str :rtype: bool """ return key in self.__casts__ def _has_set_mutator(self, key): """ Determine whether an attribute has a set mutator. :param key: The attribute :type key: str :rtype: bool """ klass = self.__class__ if key not in self._mutator_cache[klass]: return False return self._mutator_cache[klass][key].mutator is not None def _is_json_castable(self, key): """ Determine whether a value is JSON castable. :param key: The key to check :type key: str :rtype: bool """ if self._has_cast(key): type = self._get_cast_type(key) return type in ['list', 'dict', 'json', 'object'] return False def _get_cast_type(self, key): """ Get the type of the cast for a model attribute. :param key: The attribute to get the cast for :type key: str :rtype: str """ return self.__casts__[key].lower().strip() def _cast_attribute(self, key, value): """ Cast an attribute to a native Python type :param key: The attribute key :type key: str :param value: The attribute value :type value: The attribute value :rtype: mixed """ if value is None: return None type = self._get_cast_type(key) if type in ['int', 'integer']: return int(value) elif type in ['real', 'float', 'double']: return float(value) elif type in ['string', 'str']: return str(value) elif type in ['bool', 'boolean']: return bool(value) elif type in ['dict', 'list', 'json'] and isinstance(value, basestring): return json.loads(value) else: return value def get_dates(self): """ Get the attributes that should be converted to dates. :rtype: list """ defaults = [self.CREATED_AT, self.UPDATED_AT] return self.__dates__ + defaults def from_datetime(self, value): """ Convert datetime to a storable string. :param value: The datetime value :type value: pendulum.Pendulum or datetime.date or datetime.datetime :rtype: str """ date_format = self.get_connection().get_query_grammar().get_date_format() if isinstance(value, pendulum.Pendulum): return value.format(date_format) if isinstance(value, datetime.date) and not isinstance(value, (datetime.datetime)): value = pendulum.date.instance(value) return value.format(date_format) return pendulum.instance(value).format(date_format) def as_datetime(self, value): """ Return a timestamp as a datetime. :rtype: pendulum.Pendulum or pendulum.Date """ if isinstance(value, basestring): return pendulum.parse(value) if isinstance(value, (int, float)): return pendulum.from_timestamp(value) if isinstance(value, datetime.date) and not isinstance(value, (datetime.datetime)): return pendulum.date.instance(value) return pendulum.instance(value) def get_date_format(self): """ Get the format to use for timestamps and dates. :rtype: str """ return 'iso' def _format_date(self, date): """ Format a date or timestamp. :param date: The date or timestamp :type date: datetime.datetime or datetime.date or pendulum.Pendulum :rtype: str """ if date is None: return date format = self.get_date_format() if format == 'iso': if isinstance(date, basestring): return pendulum.parse(date).isoformat() return date.isoformat() else: if isinstance(date, basestring): return pendulum.parse(date).format(format) return date.strftime(format) def set_attribute(self, key, value): """ Set a given attribute on the model. """ if self._has_set_mutator(key): return super(Model, self).__setattr__(key, value) if key in self.get_dates() and value: value = self.from_datetime(value) if self._is_json_castable(key): value = json.dumps(value) self._attributes[key] = value def replicate(self, except_=None): """ Clone the model into a new, non-existing instance. :param except_: The attributes that should not be cloned :type except_: list :rtype: Model """ if except_ is None: except_ = [ self.get_key_name(), self.get_created_at_column(), self.get_updated_at_column() ] attributes = {x: self._attributes[x] for x in self._attributes if x not in except_} instance = self.new_instance(attributes) instance.set_relations(dict(**self._relations)) return instance def get_attributes(self): """ Get all of the current attributes on the model. :rtype: dict """ return self._attributes def set_raw_attributes(self, attributes, sync=False): """ Set the dictionary of model attributes. No checking is done. :param attributes: The model attributes :type attributes: dict :param sync: Whether to sync the attributes or not :type sync: bool """ self._attributes = dict(attributes.items()) if sync: self.sync_original() def set_raw_attribute(self, key, value, sync=False): """ Set an attribute. No checking is done. :param key: The attribute name :type key: str :param value: The attribute value :type value: mixed :param sync: Whether to sync the attributes or not :type sync: bool """ self._attributes[key] = value if sync: self.sync_original() def get_original(self, key=None, default=None): """ Get the original values :param key: The original key to get :type key: str :param default: The default value if the key does not exist :type default: mixed :rtype: mixed """ if key is None: return self._original return self._original.get(key, default) def sync_original(self): """ Sync the original attributes with the current. :rtype: Builder """ self._original = dict(self._attributes.items()) return self def sync_original_attribute(self, attribute): """ Sync a single original attribute with its current value. :param attribute: The attribute to sync :type attribute: str :rtype: Model """ self._original[attribute] = self._attributes[attribute] return self def is_dirty(self, *attributes): """ Determine if the model or given attributes have been modified. :param attributes: The attributes to check :type attributes: list :rtype: boolean """ dirty = self.get_dirty() if not attributes: return len(dirty) > 0 for attribute in attributes: if attribute in dirty: return True return False def get_dirty(self): """ Get the attribute that have been change since last sync. :rtype: list """ dirty = {} for key, value in self._attributes.items(): if key not in self._original: dirty[key] = value elif value != self._original[key]: dirty[key] = value return dirty @property def exists(self): return self._exists def set_exists(self, exists): self._exists = exists def set_appends(self, appends): """ Sets the appendable attributes. :param appends: The appendable attributes :type appends: list """ self.__appends__ = appends return self def get_relations(self): """ Get all the loaded relations for the instance. :rtype: dict """ return self._relations def get_relation(self, relation): """ Get a specific relation. :param relation: The name of the relation. :type relation: str :rtype: mixed """ return self._relations[relation] def set_relation(self, relation, value): """ Set the specific relation in the model. :param relation: The name of the relation :type relation: str :param value: The relation :type value: mixed :return: The current Model instance :rtype: Model """ self._relations[relation] = value return self def set_relations(self, relations): self._relations = relations return self def get_connection(self): """ Get the database connection for the model :rtype: orator.connections.Connection """ return self.resolve_connection(self.__connection__) def get_connection_name(self): """ Get the database connection name for the model. :rtype: str """ return self.__connection__ def set_connection(self, name): """ Set the connection associated with the model. :param name: The connection name :type name: str :return: The current model instance :rtype: Model """ self.__connection__ = name return self @classmethod def resolve_connection(cls, connection=None): """ Resolve a connection instance. :param connection: The connection name :type connection: str :rtype: orator.connections.Connection """ return cls.__resolver.connection(connection) @classmethod def get_connection_resolver(cls): """ Get the connection resolver instance. """ return cls.__resolver @classmethod def set_connection_resolver(cls, resolver): """ Set the connection resolver instance. """ cls.__resolver = resolver @classmethod def unset_connection_resolver(cls): """ Unset the connection resolver instance. """ cls._resolver = None def _get_mutated_attributes(self): """ Get the mutated attributes. :return: list """ klass = self.__class__ if klass in self._accessor_cache: return self._accessor_cache[klass] return [] def __getattr__(self, item): return self.get_attribute(item) def __setattr__(self, key, value): if key in ['_attributes', '_exists', '_relations', '_original'] or key.startswith('__'): return object.__setattr__(self, key, value) if self._has_set_mutator(key): return self.set_attribute(key, value) try: if object.__getattribute__(self, key): return object.__setattr__(self, key, value) except AttributeError: pass if callable(getattr(self, key, None)): return super(Model, self).__setattr__(key, value) else: self.set_attribute(key, value) def __delattr__(self, item): try: super(Model, self).__delattr__(item) except AttributeError: del self._attributes[item] def __getstate__(self): return { 'attributes': self._attributes, 'relations': self._relations, 'exists': self._exists } def __setstate__(self, state): self._boot_if_not_booted() self.set_raw_attributes(state['attributes'], True) self.set_relations(state['relations']) self.set_exists(state['exists']) PK!>&xss orator/orm/relations/__init__.py# -*- coding: utf-8 -*- from .relation import Relation from .has_one import HasOne from .has_many import HasMany from .belongs_to import BelongsTo from .belongs_to_many import BelongsToMany from .has_many_through import HasManyThrough from .morph_one import MorphOne from .morph_many import MorphMany from .morph_to import MorphTo from .morph_to_many import MorphToMany PK! m@"orator/orm/relations/belongs_to.py# -*- coding: utf-8 -*- from ...query.expression import QueryExpression from .relation import Relation from .result import Result class BelongsTo(Relation): def __init__(self, query, parent, foreign_key, other_key, relation): """ :param query: A Builder instance :type query: Builder :param parent: The parent model :type parent: Model :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :param relation: The relation name :type relation: str """ self._other_key = other_key self._relation = relation self._foreign_key = foreign_key super(BelongsTo, self).__init__(query, parent) def get_results(self): """ Get the results of the relationship. """ if self._query is None: return None return self._query.first() def add_constraints(self): """ Set the base constraints on the relation query. :rtype: None """ if self._constraints: foreign_key = getattr(self._parent, self._foreign_key, None) if foreign_key is None: self._query = None else: table = self._related.get_table() self._query.where('{}.{}'.format(table, self._other_key), '=', foreign_key) def get_relation_count_query(self, query, parent): """ Add the constraints for a relationship count query. :type query: orator.orm.Builder :type parent: orator.orm.Builder :rtype: Builder """ query.select(QueryExpression('COUNT(*)')) other_key = self.wrap('%s.%s' % (query.get_model().get_table(), self._other_key)) return query.where(self.get_qualified_foreign_key(), '=', QueryExpression(other_key)) def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ key = '%s.%s' % (self._related.get_table(), self._other_key) self._query.where_in(key, self._get_eager_model_keys(models)) def _get_eager_model_keys(self, models): """ Gather the keys from a list of related models. :type models: list :rtype: list """ keys = [] for model in models: value = getattr(model, self._foreign_key) if value is not None and value not in keys: keys.append(value) if not len(keys): return [0] return keys def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(None, self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ foreign = self._foreign_key other = self._other_key dictionary = {} for result in results: dictionary[result.get_attribute(other)] = result for model in models: value = getattr(model, foreign) if value in dictionary: results = Result(dictionary[value], self, model) else: results = Result(None, self, model) model.set_relation(relation, results) return models def associate(self, model): """ Associate the model instance to the given parent. :type model: orator.Model :rtype: orator.Model """ self._parent.set_attribute(self._foreign_key, model.get_attribute(self._other_key)) return self._parent.set_relation(self._relation, Result(model, self, self._parent)) def dissociate(self): """ Dissociate previously associated model from the given parent. :rtype: orator.Model """ self._parent.set_attribute(self._foreign_key, None) return self._parent.set_relation(self._relation, Result(None, self, self._parent)) def update(self, _attributes=None, **attributes): """ Update the parent model on the relationship. :param attributes: The update attributes :type attributes: dict :rtype: mixed """ if _attributes is not None: attributes.update(_attributes) instance = self.get_results() return instance.fill(attributes).save() def get_foreign_key(self): return self._foreign_key def get_qualified_foreign_key(self): return '%s.%s' % (self._parent.get_table(), self._foreign_key) def get_other_key(self): return self._other_key def get_qualified_other_key_name(self): return '%s.%s' % (self._related.get_table(), self._other_key) def _new_instance(self, model): return BelongsTo( self.new_query(), model, self._foreign_key, self._other_key, self._relation ) PK!\NZZZZ'orator/orm/relations/belongs_to_many.py# -*- coding: utf-8 -*- import hashlib import time import inflection from ...exceptions.orm import ModelNotFound from ...query.expression import QueryExpression from ..collection import Collection import orator.orm.model from .relation import Relation from .result import Result class BelongsToMany(Relation): _table = None _other_key = None _foreign_key = None _relation_name = None _pivot_columns = [] _pivot_wheres = [] def __init__(self, query, parent, table, foreign_key, other_key, relation_name=None): """ :param query: A Builder instance :type query: Builder :param parent: The parent model :type parent: Model :param table: The pivot table :type table: str :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :param relation_name: The relation name :type relation_name: str """ self._table = table self._other_key = other_key self._foreign_key = foreign_key self._relation_name = relation_name self._pivot_columns = [] self._pivot_wheres = [] super(BelongsToMany, self).__init__(query, parent) def get_results(self): """ Get the results of the relationship. """ return self.get() def where_pivot(self, column, operator=None, value=None, boolean='and'): """ Set a where clause for a pivot table column. :param column: The column of the where clause, can also be a QueryBuilder instance for sub where :type column: str|Builder :param operator: The operator of the where clause :type operator: str :param value: The value of the where clause :type value: mixed :param boolean: The boolean of the where clause :type boolean: str :return: self :rtype: self """ self._pivot_wheres.append([column, operator, value, boolean]) return self._query.where('%s.%s' % (self._table, column), operator, value, boolean) def or_where_pivot(self, column, operator=None, value=None): """ Set an or where clause for a pivot table column. :param column: The column of the where clause, can also be a QueryBuilder instance for sub where :type column: str|Builder :param operator: The operator of the where clause :type operator: str :param value: The value of the where clause :type value: mixed :return: self :rtype: BelongsToMany """ return self.where_pivot(column, operator, value, 'or') def first(self, columns=None): """ Execute the query and get the first result. :type columns: list """ self._query.take(1) results = self.get(columns) if len(results) > 0: return results.first() return def first_or_fail(self, columns=None): """ Execute the query and get the first result or raise an exception. :type columns: list :raises: ModelNotFound """ model = self.first(columns) if model is not None: return model raise ModelNotFound(self._parent.__class__) def get(self, columns=None): """ Execute the query as a "select" statement. :type columns: list :rtype: orator.Collection """ if columns is None: columns = ['*'] if self._query.get_query().columns: columns = [] select = self._get_select_columns(columns) models = self._query.add_select(*select).get_models() self._hydrate_pivot_relation(models) if len(models) > 0: models = self._query.eager_load_relations(models) return self._related.new_collection(models) def _hydrate_pivot_relation(self, models): """ Hydrate the pivot table relationship on the models. :type models: list """ for model in models: pivot = self.new_existing_pivot(self._clean_pivot_attributes(model)) model.set_relation('pivot', pivot) def _clean_pivot_attributes(self, model): """ Get the pivot attributes from a model. :type model: orator.Model """ values = {} delete_keys = [] for key, value in model.get_attributes().items(): if key.find('pivot_') == 0: values[key[6:]] = value delete_keys.append(key) for key in delete_keys: delattr(model, key) return values def add_constraints(self): """ Set the base constraints on the relation query. :rtype: None """ self._set_join() if BelongsToMany._constraints: self._set_where() def get_relation_count_query(self, query, parent): """ Add the constraints for a relationship count query. :type query: orator.orm.Builder :type parent: orator.orm.Builder :rtype: orator.orm.Builder """ if parent.get_query().from__ == query.get_query().from__: return self.get_relation_count_query_for_self_join(query, parent) self._set_join(query) return super(BelongsToMany, self).get_relation_count_query(query, parent) def get_relation_count_query_for_self_join(self, query, parent): """ Add the constraints for a relationship count query on the same table. :type query: orator.orm.Builder :type parent: orator.orm.Builder :rtype: orator.orm.Builder """ query.select(QueryExpression('COUNT(*)')) table_prefix = self._query.get_query().get_connection().get_table_prefix() hash_ = self.get_relation_count_hash() query.from_('%s AS %s%s' % (self._table, table_prefix, hash_)) key = self.wrap(self.get_qualified_parent_key_name()) return query.where('%s.%s' % (hash_, self._foreign_key), '=', QueryExpression(key)) def get_relation_count_hash(self): """ Get a relationship join table hash. :rtype: str """ return 'self_%s' % (hashlib.md5(str(time.time()).encode()).hexdigest()) def _get_select_columns(self, columns=None): """ Set the select clause for the relation query. :param columns: The columns :type columns: list :rtype: list """ if columns == ['*'] or columns is None: columns = ['%s.*' % self._related.get_table()] return columns + self._get_aliased_pivot_columns() def _get_aliased_pivot_columns(self): """ Get the pivot columns for the relation. :rtype: list """ defaults = [self._foreign_key, self._other_key] columns = [] for column in defaults + self._pivot_columns: value = '%s.%s AS pivot_%s' % (self._table, column, column) if value not in columns: columns.append('%s.%s AS pivot_%s' % (self._table, column, column)) return columns def _has_pivot_column(self, column): """ Determine whether the given column is defined as a pivot column. :param column: The column to check :type column: str :rtype: bool """ return column in self._pivot_columns def _set_join(self, query=None): """ Set the join clause for the relation query. :param query: The query builder :type query: orator.orm.Builder :return: self :rtype: BelongsToMany """ if not query: query = self._query base_table = self._related.get_table() key = '%s.%s' % (base_table, self._related.get_key_name()) query.join(self._table, key, '=', self.get_other_key()) return self def _set_where(self): """ Set the where clause for the relation query. :return: self :rtype: BelongsToMany """ foreign = self.get_foreign_key() self._query.where(foreign, '=', self._parent.get_key()) return self def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ self._query.where_in(self.get_foreign_key(), self.get_keys(models)) def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(self._related.new_collection(), self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ dictionary = self._build_dictionary(results) for model in models: key = model.get_key() if key in dictionary: collection = Result(self._related.new_collection(dictionary[key]), self, model) else: collection = Result(self._related.new_collection(), self, model) model.set_relation(relation, collection) return models def _build_dictionary(self, results): """ Build model dictionary keyed by the relation's foreign key. :param results: The results :type results: Collection :rtype: dict """ foreign = self._foreign_key dictionary = {} for result in results: key = getattr(result.pivot, foreign) if key not in dictionary: dictionary[key] = [] dictionary[key].append(result) return dictionary def touch(self): """ Touch all of the related models of the relationship. """ key = self.get_related().get_key_name() columns = self.get_related_fresh_update() ids = self.get_related_ids() if len(ids) > 0: self.get_related().new_query().where_in(key, ids).update(columns) def get_related_ids(self): """ Get all of the IDs for the related models. :rtype: list """ related = self.get_related() full_key = related.get_qualified_key_name() return self.get_query().select(full_key).lists(related.get_key_name()) def save(self, model, joining=None, touch=True): """ Save a new model and attach it to the parent model. :type model: orator.Model :type joining: dict :type touch: bool :rtype: orator.Model """ if joining is None: joining = {} model.save({'touch': False}) self.attach(model.get_key(), joining, touch) return model def save_many(self, models, joinings=None): """ Save a list of new models and attach them to the parent model :type models: list :type joinings: dict :rtype: list """ if joinings is None: joinings = {} for key, model in enumerate(models): self.save(model, joinings.get(key), False) self.touch_if_touching() return models def find_or_new(self, id, columns=None): """ Find a model by its primary key or return new instance of the related model. :param id: The primary key :type id: mixed :param columns: The columns to retrieve :type columns: list :rtype: Collection or Model """ instance = self._query.find(id, columns) if instance is None: instance = self.get_related().new_instance() return instance def first_or_new(self, _attributes=None, **attributes): """ Get the first related model record matching the attributes or instantiate it. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._query.where(attributes).first() if instance is None: instance = self._related.new_instance() return instance def first_or_create(self, _attributes=None, _joining=None, _touch=True, **attributes): """ Get the first related model record matching the attributes or create it. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._query.where(attributes).first() if instance is None: instance = self.create(attributes, _joining or {}, _touch) return instance def update_or_create(self, attributes, values=None, joining=None, touch=True): """ Create or update a related record matching the attributes, and fill it with values. :param attributes: The attributes :type attributes: dict :param values: The values :type values: dict :rtype: Model """ if values is None: values = {} instance = self._query.where(attributes).first() if instance is None: return self.create(values, joining, touch) instance.fill(**values) instance.save({'touch': False}) return instance def create(self, _attributes=None, _joining=None, _touch=True, **attributes): """ Create a new instance of the related model. :param attributes: The attributes :type attributes: dict :rtype: orator.orm.Model """ if _attributes is not None: attributes.update(_attributes) instance = self._related.new_instance(attributes) instance.save({'touch': False}) self.attach(instance.get_key(), _joining, _touch) return instance def create_many(self, records, joinings=None): """ Create a list of new instances of the related model. """ if joinings is None: joinings = [] instances = [] for key, record in enumerate(records): instances.append(self.create(record), joinings[key], False) self.touch_if_touching() return instances def sync(self, ids, detaching=True): """ Sync the intermediate tables with a list of IDs or collection of models """ changes = { 'attached': [], 'detached': [], 'updated': [] } if isinstance(ids, Collection): ids = ids.model_keys() current = self._new_pivot_query().lists(self._other_key).all() records = self._format_sync_list(ids) detach = [x for x in current if x not in records.keys()] if detaching and len(detach) > 0: self.detach(detach) changes['detached'] = detach changes.update(self._attach_new(records, current, False)) if len(changes['attached']) or len(changes['updated']): self.touch_if_touching() return changes def _format_sync_list(self, records): """ Format the sync list so that it is keyed by ID. """ results = {} for attributes in records: if not isinstance(attributes, dict): id, attributes = attributes, {} else: id = list(attributes.keys())[0] attributes = attributes[id] results[id] = attributes return results def _attach_new(self, records, current, touch=True): """ Attach all of the IDs that aren't in the current dict. """ changes = { 'attached': [], 'updated': [] } for id, attributes in records.items(): if id not in current: self.attach(id, attributes, touch) changes['attached'].append(id) elif len(attributes) > 0 and self.update_existing_pivot(id, attributes, touch): changes['updated'].append(id) return changes def update_existing_pivot(self, id, attributes, touch=True): """ Update an existing pivot record on the table. """ if self.updated_at() in self._pivot_columns: attributes = self.set_timestamps_on_attach(attributes, True) updated = self.new_pivot_statement_for_id(id).update(attributes) if touch: self.touch_if_touching() return updated def attach(self, id, attributes=None, touch=True): """ Attach a model to the parent. """ if isinstance(id, orator.orm.Model): id = id.get_key() query = self.new_pivot_statement() if not isinstance(id, list): id = [id] query.insert(self._create_attach_records(id, attributes)) if touch: self.touch_if_touching() def _create_attach_records(self, ids, attributes): """ Create a list of records to insert into the pivot table. """ records = [] timed = (self._has_pivot_column(self.created_at()) or self._has_pivot_column(self.updated_at())) for key, value in enumerate(ids): records.append(self._attacher(key, value, attributes, timed)) return records def _attacher(self, key, value, attributes, timed): """ Create a full attachment record payload. """ id, extra = self._get_attach_id(key, value, attributes) record = self._create_attach_record(id, timed) if extra: record.update(extra) return record def _get_attach_id(self, key, value, attributes): """ Get the attach record ID and extra attributes. """ if isinstance(value, dict): key = list(value.keys())[0] attributes.update(value[key]) return [key, attributes] return value, attributes def _create_attach_record(self, id, timed): """ Create a new pivot attachement record. """ record = {} record[self._foreign_key] = self._parent.get_key() record[self._other_key] = id if timed: record = self._set_timestamps_on_attach(record) return record def _set_timestamps_on_attach(self, record, exists=False): """ Set the creation an update timestamps on an attach record. """ fresh = self._parent.fresh_timestamp() if not exists and self._has_pivot_column(self.created_at()): record[self.created_at()] = fresh if self._has_pivot_column(self.updated_at()): record[self.updated_at()] = fresh return record def detach(self, ids=None, touch=True): """ Detach models from the relationship. """ if isinstance(ids, orator.orm.model.Model): ids = ids.get_key() if ids is None: ids = [] query = self._new_pivot_query() if not isinstance(ids, list): ids = [ids] if len(ids) > 0: query.where_in(self._other_key, ids) if touch: self.touch_if_touching() results = query.delete() return results def touch_if_touching(self): """ Touch if the parent model is being touched. """ if self._touching_parent(): self.get_parent().touch() if self.get_parent().touches(self._relation_name): self.touch() def _touching_parent(self): """ Determine if we should touch the parent on sync. """ return self.get_related().touches(self._guess_inverse_relation()) def _guess_inverse_relation(self): return inflection.camelize(inflection.pluralize(self.get_parent().__class__.__name__)) def _new_pivot_query(self): """ Create a new query builder for the pivot table. :rtype: orator.orm.Builder """ query = self.new_pivot_statement() for where_args in self._pivot_wheres: query.where(*where_args) return query.where(self._foreign_key, self._parent.get_key()) def new_pivot_statement(self): """ Get a new plain query builder for the pivot table. """ return self._query.get_query().new_query().from_(self._table) def new_pivot_statement_for_id(self, id): """ Get a new pivot statement for a given "other" id. """ return self._new_pivot_query().where(self._other_key, id) def new_pivot(self, attributes=None, exists=False): """ Create a new pivot model instance. """ pivot = self._related.new_pivot(self._parent, attributes, self._table, exists) return pivot.set_pivot_keys(self._foreign_key, self._other_key) def new_existing_pivot(self, attributes): """ Create a new existing pivot model instance. """ return self.new_pivot(attributes, True) def with_pivot(self, *columns): """ Set the columns on the pivot table to retrieve. """ columns = list(columns) self._pivot_columns += columns return self def with_timestamps(self, created_at=None, updated_at=None): """ Specify that the pivot table has creation and update columns. """ if not created_at: created_at = self.created_at() if not updated_at: updated_at = self.updated_at() return self.with_pivot(created_at, updated_at) def get_related_fresh_update(self): """ Get the related model's update at column at """ return {self._related.get_updated_at_column(): self._related.fresh_timestamp()} def get_has_compare_key(self): """ Get the key for comparing against the parent key in "has" query. """ return self.get_foreign_key() def get_foreign_key(self): return '%s.%s' % (self._table, self._foreign_key) def get_other_key(self): return '%s.%s' % (self._table, self._other_key) def get_table(self): return self._table def get_relation_name(self): return self._relation_name def _new_instance(self, model): relation = BelongsToMany( self.new_query(), model, self._table, self._foreign_key, self._other_key, self._relation_name ) relation.with_pivot(*self._pivot_columns) return relation PK!I orator/orm/relations/has_many.py# -*- coding: utf-8 -*- from .has_one_or_many import HasOneOrMany from .result import Result class HasMany(HasOneOrMany): def get_results(self): """ Get the results of the relationship. """ return self._query.get() def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(self._related.new_collection(), self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ return self.match_many(models, results, relation) def _new_instance(self, model): return HasMany( self.new_query(), model, self._foreign_key, self._local_key ) PK!QLkyy(orator/orm/relations/has_many_through.py# -*- coding: utf-8 -*- from ...query.expression import QueryExpression from .relation import Relation from .result import Result class HasManyThrough(Relation): def __init__(self, query, far_parent, parent, first_key, second_key): """ :param query: A Builder instance :type query: Builder :param far_parent: The far parent model :type far_parent: Model :param parent: The parent model :type parent: Model :type first_key: str :type second_key: str """ self._first_key = first_key self._second_key = second_key self._far_parent = far_parent super(HasManyThrough, self).__init__(query, parent) def add_constraints(self): """ Set the base constraints on the relation query. :rtype: None """ parent_table = self._parent.get_table() self._set_join() if self._constraints: self._query.where('%s.%s' % (parent_table, self._first_key), '=', self._far_parent.get_key()) def get_relation_count_query(self, query, parent): """ Add the constraints for a relationship count query. :type query: Builder :type parent: Builder :rtype: Builder """ parent_table = self._parent.get_table() self._set_join(query) query.select(QueryExpression('COUNT(*)')) key = self.wrap('%s.%s' % (parent_table, self._first_key)) return query.where(self.get_has_compare_key(), '=', QueryExpression(key)) def _set_join(self, query=None): """ Set the join clause for the query. """ if not query: query = self._query foreign_key = '%s.%s' % (self._related.get_table(), self._second_key) query.join(self._parent.get_table(), self.get_qualified_parent_key_name(), '=', foreign_key) def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ table = self._parent.get_table() self._query.where_in('%s.%s' % (table, self._first_key), self.get_keys(models)) def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(self._related.new_collection(), self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ dictionary = self._build_dictionary(results) for model in models: key = model.get_key() if key in dictionary: value = Result(self._related.new_collection(dictionary[key]), self, model) else: value = Result(self._related.new_collection(), self, model) model.set_relation(relation, value) return models def _build_dictionary(self, results): """ Build model dictionary keyed by the relation's foreign key. :param results: The results :type results: Collection :rtype: dict """ foreign = self._first_key dictionary = {} for result in results: key = getattr(result, foreign) if key not in dictionary: dictionary[key] = [] dictionary[key].append(result) return dictionary def get_results(self): """ Get the results of the relationship. """ return self.get() def get(self, columns=None): """ Execute the query as a "select" statement. :type columns: list :rtype: orator.Collection """ if columns is None: columns = ['*'] select = self._get_select_columns(columns) models = self._query.add_select(*select).get_models() if len(models) > 0: models = self._query.eager_load_relations(models) return self._related.new_collection(models) def _get_select_columns(self, columns=None): """ Set the select clause for the relation query. :param columns: The columns :type columns: list :rtype: list """ if columns == ['*'] or columns is None: columns = ['%s.*' % self._related.get_table()] return columns + ['%s.%s' % (self._parent.get_table(), self._first_key)] def get_has_compare_key(self): return self._far_parent.get_qualified_key_name() def _new_instance(self, model): return HasManyThrough( self.new_query(), model, self._parent, self._first_key, self._second_key ) PK!'9orator/orm/relations/has_one.py# -*- coding: utf-8 -*- from .has_one_or_many import HasOneOrMany from .result import Result class HasOne(HasOneOrMany): def get_results(self): """ Get the results of the relationship. """ return self._query.first() def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(None, self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ return self.match_one(models, results, relation) def _new_instance(self, model): return HasOne( self.new_query(), model, self._foreign_key, self._local_key ) PK!In!n!'orator/orm/relations/has_one_or_many.py# -*- coding: utf-8 -*- from ..collection import Collection from .relation import Relation from .result import Result class HasOneOrMany(Relation): def __init__(self, query, parent, foreign_key, local_key): """ :type query: orator.orm.Builder :param parent: The parent model :type parent: Model :param foreign_key: The foreign key of the parent model :type foreign_key: str :param local_key: The local key of the parent model :type local_key: str """ self._local_key = local_key self._foreign_key = foreign_key super(HasOneOrMany, self).__init__(query, parent) def add_constraints(self): """ Set the base constraints of the relation query """ if self._constraints: self._query.where(self._foreign_key, '=', self.get_parent_key()) def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ return self._query.where_in(self._foreign_key, self.get_keys(models, self._local_key)) def match_one(self, models, results, relation): """ Match the eargerly loaded resuls to their single parents. :param models: The parents :type models: list :param results: The results collection :type results: Collection :param relation: The relation :type relation: str :rtype: list """ return self._match_one_or_many(models, results, relation, 'one') def match_many(self, models, results, relation): """ Match the eargerly loaded resuls to their single parents. :param models: The parents :type models: list :param results: The results collection :type results: Collection :param relation: The relation :type relation: str :rtype: list """ return self._match_one_or_many(models, results, relation, 'many') def _match_one_or_many(self, models, results, relation, type_): """ Match the eargerly loaded resuls to their single parents. :param models: The parents :type models: list :param results: The results collection :type results: Collection :param relation: The relation :type relation: str :param type_: The match type :type type_: str :rtype: list """ dictionary = self._build_dictionary(results) for model in models: key = model.get_attribute(self._local_key) if key in dictionary: value = Result(self._get_relation_value(dictionary, key, type_), self, model) else: if type_ == 'one': value = Result(None, self, model) else: value = Result(self._related.new_collection(), self, model) model.set_relation(relation, value) return models def _get_relation_value(self, dictionary, key, type): """ Get the value of the relationship by one or many type. :type dictionary: dict :type key: str :type type: str """ value = dictionary[key] if type == 'one': return value[0] return self._related.new_collection(value) def _build_dictionary(self, results): """ Build model dictionary keyed by the relation's foreign key. :param results: The results :type results: Collection :rtype: dict """ dictionary = {} foreign = self.get_plain_foreign_key() for result in results: key = getattr(result, foreign) if key not in dictionary: dictionary[key] = [] dictionary[key].append(result) return dictionary def save(self, model): """ Attach a model instance to the parent models. :param model: The model instance to attach :type model: Model :rtype: Model """ model.set_attribute(self.get_plain_foreign_key(), self.get_parent_key()) if model.save(): return model return False def save_many(self, models): """ Attach a list of models to the parent instance. :param models: The models to attach :type models: list of Model :rtype: list """ return list(map(self.save, models)) def find_or_new(self, id, columns=None): """ Find a model by its primary key or return new instance of the related model. :param id: The primary key :type id: mixed :param columns: The columns to retrieve :type columns: list :rtype: Collection or Model """ if columns is None: columns = ['*'] instance = self._query.find(id, columns) if instance is None: instance = self._related.new_instance() instance.set_attribute(self.get_plain_foreign_key(), self.get_parent_key()) return instance def first_or_new(self, _attributes=None, **attributes): """ Get the first related model record matching the attributes or instantiate it. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._query.where(attributes).first() if instance is None: instance = self._related.new_instance() instance.set_attribute(self.get_plain_foreign_key(), self.get_parent_key()) return instance def first_or_create(self, _attributes=None, **attributes): """ Get the first related record matching the attributes or create it. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._query.where(attributes).first() if instance is None: instance = self.create(**attributes) return instance def update_or_create(self, attributes, values=None): """ Create or update a related record matching the attributes, and fill it with values. :param attributes: The attributes :type attributes: dict :param values: The values :type values: dict :rtype: Model """ instance = self.first_or_new(**attributes) instance.fill(values) instance.save() return instance def create(self, _attributes=None, **attributes): """ Create a new instance of the related model. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._related.new_instance(attributes) instance.set_attribute(self.get_plain_foreign_key(), self.get_parent_key()) instance.save() return instance def create_many(self, records): """ Create a list of new instances of the related model. :param records: instances attributes :type records: list :rtype: list """ instances = [] for record in records: instances.append(self.create(**record)) return instances def update(self, _attributes=None, **attributes): """ Perform an update on all the related models. :param attributes: The attributes :type attributes: dict :rtype: int """ if _attributes is not None: attributes.update(_attributes) if self._related.uses_timestamps(): attributes[self.get_related_updated_at()] = self._related.fresh_timestamp() return self._query.update(attributes) def get_has_compare_key(self): return self.get_foreign_key() def get_foreign_key(self): return self._foreign_key def get_plain_foreign_key(self): segments = self.get_foreign_key().split('.') return segments[-1] def get_parent_key(self): return self._parent.get_attribute(self._local_key) def get_qualified_parent_key_name(self): return '%s.%s' % (self._parent.get_table(), self._local_key) PK!1ff"orator/orm/relations/morph_many.py# -*- coding: utf-8 -*- from .morph_one_or_many import MorphOneOrMany from .result import Result class MorphMany(MorphOneOrMany): def get_results(self): """ Get the results of the relationship. """ return self._query.get() def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(self._related.new_collection(), self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ return self.match_many(models, results, relation) PK!?LL!orator/orm/relations/morph_one.py# -*- coding: utf-8 -*- from .morph_one_or_many import MorphOneOrMany from .result import Result class MorphOne(MorphOneOrMany): def get_results(self): """ Get the results of the relationship. """ return self._query.first() def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ for model in models: model.set_relation(relation, Result(None, self, model)) return models def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ return self.match_one(models, results, relation) PK!sy)orator/orm/relations/morph_one_or_many.py# -*- coding: utf-8 -*- from .has_one_or_many import HasOneOrMany class MorphOneOrMany(HasOneOrMany): def __init__(self, query, parent, morph_type, foreign_key, local_key): """ :type query: orator.orm.Builder :param parent: The parent model :type parent: Model :param morph_type: The type of the morph :type morph_type: str :param foreign_key: The foreign key of the parent model :type foreign_key: str :param local_key: The local key of the parent model :type local_key: str """ self._morph_type = morph_type self._morph_name = parent.get_morph_name() super(MorphOneOrMany, self).__init__(query, parent, foreign_key, local_key) def add_constraints(self): """ Set the base constraints of the relation query """ if self._constraints: super(MorphOneOrMany, self).add_constraints() self._query.where(self._morph_type, self._morph_name) def get_relation_count_query(self, query, parent): """ Add the constraints for a relationship count query. :type query: Builder :type parent: Builder :rtype: Builder """ query = super(MorphOneOrMany, self).get_relation_count_query(query, parent) return query.where(self._morph_type, self._morph_name) def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ super(MorphOneOrMany, self).add_eager_constraints(models) self._query.where(self._morph_type, self._morph_name) def save(self, model): """ Attach a model instance to the parent models. :param model: The model instance to attach :type model: Model :rtype: Model """ model.set_attribute(self.get_plain_morph_type(), self._morph_name) return super(MorphOneOrMany, self).save(model) def find_or_new(self, id, columns=None): """ Find a model by its primary key or return new instance of the related model. :param id: The primary key :type id: mixed :param columns: The columns to retrieve :type columns: list :rtype: Collection or Model """ if columns is None: columns = ['*'] instance = self._query.find(id, columns) if instance is None: instance = self._related.new_instance() self._set_foreign_attributes_for_create(instance) return instance def first_or_new(self, _attributes=None, **attributes): """ Get the first related model record matching the attributes or instantiate it. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._query.where(attributes).first() if instance is None: instance = self._related.new_instance() self._set_foreign_attributes_for_create(instance) return instance def first_or_create(self, _attributes=None, **attributes): """ Get the first related record matching the attributes or create it. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._query.where(attributes).first() if instance is None: instance = self.create(**attributes) return instance def update_or_create(self, attributes, values=None): """ Create or update a related record matching the attributes, and fill it with values. :param attributes: The attributes :type attributes: dict :param values: The values :type values: dict :rtype: Model """ instance = self.first_or_new(**attributes) instance.fill(values) instance.save() return instance def create(self, _attributes=None, **attributes): """ Create a new instance of the related model. :param attributes: The attributes :type attributes: dict :rtype: Model """ if _attributes is not None: attributes.update(_attributes) instance = self._related.new_instance(attributes) self._set_foreign_attributes_for_create(instance) instance.save() return instance def _set_foreign_attributes_for_create(self, model): """ Set the foreign ID and type for creation a related model. """ model.set_attribute(self.get_plain_foreign_key(), self.get_parent_key()) model.set_attribute(self.get_plain_morph_type(), self._morph_name) def get_morph_type(self): return self._morph_type def get_plain_morph_type(self): return self._morph_type.split('.')[-1] def get_morph_name(self): return self._morph_name def _new_instance(self, parent): return self.__class__( self.new_query(), parent, self._morph_type, self._foreign_key, self._local_key ) PK!T<#orator/orm/relations/morph_pivot.py# -*- coding: utf-8 -*- from .pivot import Pivot class MorphPivot(Pivot): _morph_name = None _morph_type = None def _set_keys_for_save_query(self, query): """ Set the keys for a save update query. :param query: A Builder instance :type query: orator.orm.Builder :return: The Builder instance :rtype: orator.orm.Builder """ query.where(self._morph_type, self._morph_name) return super(MorphPivot, self)._set_keys_for_save_query(query) def delete(self): """ Delete the pivot model record from the database. :rtype: int """ query = self._get_delete_query() query.where(self._morph_type, self._morph_name) return query.delete() def set_morph_type(self, morph_type): self._morph_type = morph_type return self def set_morph_name(self, morph_name): self._morph_name = morph_name return self PK!^" orator/orm/relations/morph_to.py# -*- coding: utf-8 -*- from .belongs_to import BelongsTo from ..collection import Collection from ...support.collection import Collection as BaseCollection from .result import Result class MorphTo(BelongsTo): def __init__(self, query, parent, foreign_key, other_key, type, relation): """ :type query: orator.orm.Builder :param parent: The parent model :type parent: Model :param query: :param parent: :param foreign_key: The foreign key of the parent model :type foreign_key: str :param other_key: The local key of the parent model :type other_key: str :param type: The morph type :type type: str :param relation: The relation name :type relation: str """ self._morph_type = type self._models = Collection() self._dictionary = {} self._with_trashed = False super(MorphTo, self).__init__(query, parent, foreign_key, other_key, relation) def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ self._models = Collection.make(models) self._build_dictionary(models) def _build_dictionary(self, models): """ Build a dictionary with the models. :param models: The models :type models: Collection """ for model in models: key = getattr(model, self._morph_type, None) if key: foreign = getattr(model, self._foreign_key) if key not in self._dictionary: self._dictionary[key] = {} if foreign not in self._dictionary[key]: self._dictionary[key][foreign] = [] self._dictionary[key][foreign].append(model) def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: Collection :type results: Collection :type relation: str """ return models def associate(self, model): """ Associate the model instance to the given parent. :type model: orator.Model :rtype: orator.Model """ self._parent.set_attribute(self._foreign_key, model.get_key()) self._parent.set_attribute(self._morph_type, model.get_morph_name()) return self._parent.set_relation(self._relation, Result(model, self, self._parent)) def get_eager(self): """ Get the relationship for eager loading. :rtype: Collection """ for type in self._dictionary.keys(): self._match_to_morph_parents(type, self._get_results_by_type(type)) return self._models def _match_to_morph_parents(self, type, results): """ Match the results for a given type to their parent. :param type: The parent type :type type: str :param results: The results to match to their parent :type results: Collection """ for result in results: if result.get_key() in self._dictionary.get(type, []): for model in self._dictionary[type][result.get_key()]: model.set_relation( self._relation, Result( result, self, model, related=result ) ) def _get_results_by_type(self, type): """ Get all the relation results for a type. :param type: The type :type type: str :rtype: Collection """ instance = self._create_model_by_type(type) key = instance.get_key_name() query = instance.new_query() query = self._use_with_trashed(query) return query.where_in(key, self._gather_keys_by_type(type).all()).get() def _gather_keys_by_type(self, type): """ Gather all of the foreign keys for a given type. :param type: The type :type type: str :rtype: BaseCollection """ foreign = self._foreign_key keys = BaseCollection.make(list(self._dictionary[type].values()))\ .map(lambda models: getattr(models[0], foreign))\ .unique() return keys def _create_model_by_type(self, type): """ Create a new model instance by type. :rtype: Model """ klass = self._parent.get_actual_class_for_morph(type) return klass() def get_morph_type(self): return self._morph_type def get_dictionary(self): return self._dictionary def with_trashed(self): self._with_trashed = True self._query = self._use_with_trashed(self._query) return self def _use_with_trashed(self, query): if self._with_trashed: return query.with_trashed() return query def _new_instance(self, model, related=None): return MorphTo( self.new_query(related), model, self._foreign_key, self._other_key if not related else related.get_key_name(), self._morph_type, self._relation ) PK!6q% %orator/orm/relations/morph_to_many.py# -*- coding: utf-8 -*- from .belongs_to_many import BelongsToMany class MorphToMany(BelongsToMany): def __init__(self, query, parent, name, table, foreign_key, other_key, relation_name=None, inverse=False): """ :param query: A Builder instance :type query: elquent.orm.Builder :param parent: The parent model :type parent: Model :param table: The pivot table :type table: str :param foreign_key: The foreign key :type foreign_key: str :param other_key: The other key :type other_key: str :param relation_name: The relation name :type relation_name: str :type inverse: bool """ self._name = name self._inverse = inverse self._morph_type = name + '_type' self._morph_name = query.get_model().get_morph_name() if inverse else parent.get_morph_name() super(MorphToMany, self).__init__( query, parent, table, foreign_key, other_key, relation_name ) def _set_where(self): """ Set the where clause for the relation query. :return: self :rtype: BelongsToMany """ super(MorphToMany, self)._set_where() self._query.where('%s.%s' % (self._table, self._morph_type), self._morph_name) def get_relation_count_query(self, query, parent): """ Add the constraints for a relationship count query. :type query: orator.orm.Builder :type parent: orator.orm.Builder :rtype: orator.orm.Builder """ query = super(MorphToMany, self).get_relation_count_query(query, parent) return query.where('%s.%s' % (self._table, self._morph_type), self._morph_name) def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ super(MorphToMany, self).add_eager_constraints(models) self._query.where('%s.%s' % (self._table, self._morph_type), self._morph_name) def _create_attach_record(self, id, timed): """ Create a new pivot attachement record. """ record = super(MorphToMany, self)._create_attach_record(id, timed) record[self._morph_type] = self._morph_name return record def _new_pivot_query(self): """ Create a new query builder for the pivot table. :rtype: orator.orm.Builder """ query = super(MorphToMany, self)._new_pivot_query() return query.where(self._morph_type, self._morph_name) def new_pivot(self, attributes=None, exists=False): """ Create a new pivot model instance. """ from .morph_pivot import MorphPivot pivot = MorphPivot(self._parent, attributes, self._table, exists) pivot.set_pivot_keys(self._foreign_key, self._other_key)\ .set_morph_type(self._morph_type)\ .set_morph_name(self._morph_name) return pivot def get_morph_type(self): return self._morph_type def get_morph_name(self): return self._morph_name def _new_instance(self, model): return MorphToMany( self.new_query(), model, self._name, self._table, self._foreign_key, self._other_key, self._relation_name, self._inverse ) PK!^ orator/orm/relations/pivot.py# -*- coding: utf-8 -*- from ..model import Model class Pivot(Model): __guarded__ = [] def __init__(self, parent, attributes, table, exists=False): """ :param parent: The parent model :type parent: Model :param attributes: The pivot attributes :type attributes: dict :param table: the pivot table :type table: str :param exists: Whether the pivot exists or not :type exists: bool """ if attributes is None: attributes = {} super(Pivot, self).__init__() self.set_raw_attributes(attributes, True) self.set_table(table) self.set_connection(parent.get_connection_name()) self.__parent = parent self.set_exists(exists) self.__timestamps__ = self.has_timestamps_attributes() def _set_keys_for_save_query(self, query): """ Set the keys for a save update query. :param query: A Builder instance :type query: orator.orm.Builder :return: The Builder instance :rtype: orator.orm.Builder """ query.where(self.__foreign_key, self.get_attribute(self.__foreign_key)) return query.where(self.__other_key, self.get_attribute(self.__other_key)) def delete(self): """ Delete the pivot model record from the database. :rtype: int """ return self._get_delete_query().delete() def _get_delete_query(self): """ Get the query builder for a delete operation on the pivot. :rtype: orator.orm.Builder """ foreign = self.get_attribute(self.__foreign_key) query = self.new_query().where(self.__foreign_key, foreign) return query.where(self.__other_key, self.get_attribute(self.__other_key)) def has_timestamps_attributes(self): """ Determine if the pivot has timestamps attributes. :rtype: bool """ return self.get_created_at_column() in self.get_attributes() def get_foreign_key(self): return self.__foreign_key def get_other_key(self): return self.__other_key def set_pivot_keys(self, foreign_key, other_key): """ Set the key names for the pivot model instance """ self.__foreign_key = foreign_key self.__other_key = other_key return self def get_created_at_column(self): return self.__parent.get_created_at_column() def get_updated_at_column(self): return self.__parent.get_updated_at_column() def set_table(self, table): """ Set the table associated with the model. :param table: The table name :type table: str """ self.__table__ = table def get_table(self): """ Get the table associated with the model. :return: The name of the table :rtype: str """ return self.__table__ PK!^A orator/orm/relations/relation.py# -*- coding: utf-8 -*- from contextlib import contextmanager from ...query.expression import QueryExpression from ..collection import Collection from ..builder import Builder class Relation(object): _constraints = True def __init__(self, query, parent): """ :param query: A Builder instance :type query: orm.orator.Builder :param parent: The parent model :type parent: Model """ self._query = query self._parent = parent self._related = query.get_model() self._extra_query = None self.add_constraints() def add_constraints(self): """ Set the base constraints on the relation query. :rtype: None """ raise NotImplementedError def add_eager_constraints(self, models): """ Set the constraints for an eager load of the relation. :type models: list """ raise NotImplementedError def init_relation(self, models, relation): """ Initialize the relation on a set of models. :type models: list :type relation: str """ raise NotImplementedError def match(self, models, results, relation): """ Match the eagerly loaded results to their parents. :type models: list :type results: Collection :type relation: str """ raise NotImplementedError def get_results(self): """ Get the results of the relationship. """ raise NotImplementedError def get_eager(self): """ Get the relationship for eager loading. :rtype: Collection """ return self.get() def touch(self): """ Touch all of the related models for the relationship. """ column = self.get_related().get_updated_at_column() self.raw_update({column: self.get_related().fresh_timestamp()}) def raw_update(self, attributes=None): """ Run a raw update against the base query. :type attributes: dict :rtype: int """ if attributes is None: attributes = {} if self._query is not None: return self._query.update(attributes) def get_relation_count_query(self, query, parent): """ Add the constraints for a relationship count query. :type query: Builder :type parent: Builder :rtype: Builder """ query.select(QueryExpression('COUNT(*)')) key = self.wrap(self.get_qualified_parent_key_name()) return query.where(self.get_has_compare_key(), '=', QueryExpression(key)) @classmethod @contextmanager def no_constraints(cls, with_subclasses=False): """ Runs a callback with constraints disabled on the relation. """ cls._constraints = False if with_subclasses: for klass in cls.__subclasses__(): klass._constraints = False try: yield cls except Exception: raise finally: cls._constraints = True if with_subclasses: for klass in cls.__subclasses__(): klass._constraints = True def get_keys(self, models, key=None): """ Get all the primary keys for an array of models. :type models: list :type key: str :rtype: list """ return list(set(map(lambda value: value.get_attribute(key) if key else value.get_key(), models))) def get_query(self): return self._query def get_base_query(self): return self._query.get_query() def merge_query(self, query): if isinstance(query, Builder): query = query.get_query() self._query.merge(query) def get_parent(self): return self._parent def get_qualified_parent_key_name(self): return self._parent.get_qualified_key_name() def get_related(self): return self._related def created_at(self): """ Get the name of the "created at" column. :rtype: str """ return self._parent.get_created_at_column() def updated_at(self): """ Get the name of the "updated at" column. :rtype: str """ return self._parent.get_updated_at_column() def get_related_updated_at(self): """ Get the name of the related model's "updated at" column. :rtype: str """ return self._related.get_updated_at_column() def wrap(self, value): """ Wrap the given value with the parent's query grammar. :rtype: str """ return self._parent.new_query().get_query().get_grammar().wrap(value) def set_parent(self, parent): self._parent = parent def set_extra_query(self, query): self._extra_query = query def new_query(self, related=None): if related is None: related = self._related query = related.new_query() if self._extra_query: query.merge(self._extra_query.get_query()) return query def new_instance(self, model, **kwargs): new = self._new_instance(model, **kwargs) if self._extra_query: new.set_extra_query(self._extra_query) return new def __dynamic(self, method): attribute = getattr(self._query, method) def call(*args, **kwargs): result = attribute(*args, **kwargs) if result is self._query: return self return result if not callable(attribute): return attribute return call def __getattr__(self, item): return self.__dynamic(item) PK!sorator/orm/relations/result.py# -*- coding: utf-8 -*- from wrapt import ObjectProxy class Result(ObjectProxy): _results = None _relation = None _parent = None _kwargs = None def __init__(self, result, relation, parent, **kwargs): """ :param query: A Builder instance :type query: orm.orator.Builder :param parent: The parent model :type parent: Model """ super(Result, self).__init__(result) self._results = result self._relation = relation self._parent = parent self._kwargs = kwargs def __call__(self, *args, **kwargs): return self._relation.new_instance(self._parent, **self._kwargs) PK!7rSSorator/orm/relations/wrapper.py# -*- coding: utf-8 -*- from lazy_object_proxy import Proxy from functools import wraps def wrapped(func): @wraps(func) def _wrapper(*args, **kwargs): return Wrapper(func(*args, **kwargs)) return _wrapper class Wrapper(Proxy): """ Wrapper around a relation which provide dynamic property functionality. """ _relation = None def __init__(self, relation): """ :param relation: The underlying relation. :type relation: Relation :return: """ super(Wrapper, self).__init__(self._get_results) self._relation = relation def _get_results(self): return self._relation.get_results() def __call__(self, *args, **kwargs): return self._relation.new_instance(self._relation.get_parent()) def __repr__(self): return repr(self.__wrapped__) class BelongsToManyWrapper(Wrapper): def with_timestamps(self): self._relation.with_timestamps() return self def with_pivot(self, *columns): self._relation.with_pivot(*columns) return self PK!__orator/orm/scopes/__init__.py# -*- coding: utf-8 -*- from .scope import Scope from .soft_deleting import SoftDeletingScope PK!~t]]orator/orm/scopes/scope.py# -*- coding: utf-8 -*- class Scope(object): def apply(self, builder, model): """ Apply the scope to a given query builder. :param builder: The query builder :type builder: orator.orm.Builder :param model: The model :type model: orator.orm.Model """ raise NotImplementedError PK!Ghh"orator/orm/scopes/soft_deleting.py# -*- coding: utf-8 -*- from .scope import Scope class SoftDeletingScope(Scope): _extensions = ['force_delete', 'restore', 'with_trashed', 'only_trashed'] def apply(self, builder, model): """ Apply the scope to a given query builder. :param builder: The query builder :type builder: orator.orm.builder.Builder :param model: The model :type model: orator.orm.Model """ builder.where_null(model.get_qualified_deleted_at_column()) self.extend(builder) def extend(self, builder): """ Extend the query builder with the needed functions. :param builder: The query builder :type builder: orator.orm.builder.Builder """ for extension in self._extensions: getattr(self, '_add_%s' % extension)(builder) builder.on_delete(self._on_delete) def _on_delete(self, builder): """ The delete replacement function. :param builder: The query builder :type builder: orator.orm.builder.Builder """ column = self._get_deleted_at_column(builder) return builder.update({ column: builder.get_model().fresh_timestamp() }) def _get_deleted_at_column(self, builder): """ Get the "deleted at" column for the builder. :param builder: The query builder :type builder: orator.orm.builder.Builder :rtype: str """ if len(builder.get_query().joins) > 0: return builder.get_model().get_qualified_deleted_at_column() else: return builder.get_model().get_deleted_at_column() def _add_force_delete(self, builder): """ Add the force delete extension to the builder. :param builder: The query builder :type builder: orator.orm.builder.Builder """ builder.macro('force_delete', self._force_delete) def _force_delete(self, builder): """ The forece delete extension. :param builder: The query builder :type builder: orator.orm.builder.Builder """ return builder.get_query().delete() def _add_restore(self, builder): """ Add the restore extension to the builder. :param builder: The query builder :type builder: orator.orm.builder.Builder """ builder.macro('restore', self._restore) def _restore(self, builder): """ The restore extension. :param builder: The query builder :type builder: orator.orm.builder.Builder """ builder.with_trashed() return builder.update({ builder.get_model().get_deleted_at_column(): None }) def _add_with_trashed(self, builder): """ Add the with-trashed extension to the builder. :param builder: The query builder :type builder: orator.orm.builder.Builder """ builder.macro('with_trashed', self._with_trashed) def _with_trashed(self, builder): """ The with-trashed extension. :param builder: The query builder :type builder: orator.orm.builder.Builder """ builder.remove_global_scope(self) return builder def _add_only_trashed(self, builder): """ Add the only-trashed extension to the builder. :param builder: The query builder :type builder: orator.orm.builder.Builder """ builder.macro('only_trashed', self._only_trashed) def _only_trashed(self, builder): """ The only-trashed extension. :param builder: The query builder :type builder: orator.orm.builder.Builder """ model = builder.get_model() builder.without_global_scope(self) builder.get_query().where_not_null(model.get_qualified_deleted_at_column()) return builder PK!vh;w4w4orator/orm/utils.py# -*- coding: utf-8 -*- import types from functools import update_wrapper from .relations.wrapper import Wrapper from .builder import Builder from ..query import QueryBuilder from .relations import ( HasOne, HasMany, HasManyThrough, BelongsTo, BelongsToMany, MorphOne, MorphMany, MorphTo, MorphToMany ) class accessor(object): def __init__(self, accessor_, attribute=None): self.accessor = accessor_ self.mutator_ = None if attribute is not None: self.attribute = attribute else: if isinstance(accessor_, property): self.attribute = accessor_.fget.__name__ else: self.attribute = self.accessor.__name__ self.expr = accessor_ if accessor_ is not None: update_wrapper(self, accessor_) def __get__(self, instance, owner): if instance is None: return self.expr else: return self.accessor(instance) def __set__(self, instance, value): if self.mutator_ is None: return instance.set_attribute(self.attribute, value) self.mutator_(instance, value) def mutator(self, f): self.mutator_ = f return mutator(f, self.attribute) class mutator(object): def __init__(self, mutator_, attribute=None): self.mutator = mutator_ self.accessor_ = None self.attribute = attribute or self.mutator.__name__ def __get__(self, instance, owner): if instance is None: return self.mutator else: if self.accessor_ is None: return instance.get_attribute(self.attribute) return self.accessor_(instance) def __set__(self, instance, value): self.mutator(instance, value) def accessor(self, f): self.accessor_ = f return accessor(f, self.attribute) class column(object): def __init__(self, property_, attribute=None): self.property = property_ self.mutator_ = None self.accessor_ = None if attribute is not None: self.attribute = attribute else: if isinstance(property_, property): self.attribute = property_.fget.__name__ else: self.attribute = self.property.__name__ def __get__(self, instance, owner): if instance is None: return self.mutator_ else: if self.accessor_ is None: return instance.get_attribute(self.attribute) return self.accessor_(instance) def __set__(self, instance, value): if self.mutator_ is None: return instance.set_attribute(self.attribute, value) self.mutator_(instance, value) def mutator(self, f): self.mutator_ = f return mutator(f, self.attribute) def accessor(self, f): self.accessor_ = f return accessor(f, self.attribute) class scope(classmethod): """ Decorator to add local scopes. """ def __init__(self, method): super(scope, self).__init__(method) self._method = method self._owner = None update_wrapper(self, method) def __get__(self, instance, owner, *args, **kwargs): if instance: self._owner = None else: self._owner = owner return self def __call__(self, *args, **kwargs): if not self._owner: return self._method(self._owner, *args, **kwargs) else: return getattr(self._owner.query(), self._method.__name__)(*args, **kwargs) # Relations decorators class relation(object): """ Base relation decorator """ relation_class = None def __init__(self, func=None, relation=None): self._relation = relation self._related = None self._conditions = None self.set_func(func) def set_func(self, func): self.func = func if self._relation is None: if isinstance(func, property): self._relation = func.fget.__name__ if func else None else: self._relation = func.__name__ if func else None self.expr = func if func is not None: update_wrapper(self, func) def __get__(self, instance, owner): if instance is None: return self.expr if self._relation in instance._relations: return instance._relations[self._relation] self._related = self.func(instance) if isinstance(self._related, (Builder, QueryBuilder)): # Extra conditions on relation self._conditions = self._related self._related = self._related.get_model().__class__ relation = self._get(instance) if self._conditions: # Setting extra conditions self._set_conditions(relation) relation = Wrapper(relation) instance._relations[self._relation] = relation return relation def _get(self, instance): raise NotImplementedError() def _set_conditions(self, relation): relation.merge_query(self._conditions) relation.set_extra_query(self._conditions) def __call__(self, func): self.set_func(func) return self class has_one(relation): """ Has One relationship decorator """ relation_class = HasOne def __init__(self, foreign_key=None, local_key=None, relation=None): if isinstance(foreign_key, (types.FunctionType, types.MethodType)): func = foreign_key foreign_key = None else: func = None self._foreign_key = foreign_key self._local_key = local_key super(has_one, self).__init__(func, relation) def _get(self, instance): return instance.has_one( self._related, self._foreign_key, self._local_key, self._relation, _wrapped=False ) class morph_one(relation): """ Morph One relationship decorator """ relation_class = MorphOne def __init__(self, name, type_column=None, id_column=None, local_key=None, relation=None): if isinstance(name, (types.FunctionType, types.MethodType)): raise RuntimeError('morph_one relation requires a name') self._name = name self._type_column = type_column self._id_column = id_column self._local_key = local_key super(morph_one, self).__init__(relation=relation) def _get(self, instance): return instance.morph_one( self._related, self._name, self._type_column, self._id_column, self._local_key, self._relation, _wrapped=False ) class belongs_to(relation): """ Belongs to relationship decorator """ relation_class = BelongsTo def __init__(self, foreign_key=None, other_key=None, relation=None): if isinstance(foreign_key, (types.FunctionType, types.MethodType)): func = foreign_key foreign_key = None else: func = None self._foreign_key = foreign_key self._other_key = other_key super(belongs_to, self).__init__(func, relation) def _get(self, instance): return instance.belongs_to( self._related, self._foreign_key, self._other_key, self._relation, _wrapped=False ) def _set(self, relation): relation._foreign_key = self._foreign_key relation._other_key = self._other_key relation._relation = self._relation class morph_to(relation): """ Morph To relationship decorator """ relation_class = MorphTo def __init__(self, name=None, type_column=None, id_column=None): if isinstance(name, (types.FunctionType, types.MethodType)): func = name name = None else: func = None self._name = name self._type_column = type_column self._id_column = id_column super(morph_to, self).__init__(func, name) def _get(self, instance): return instance.morph_to( self._relation, self._type_column, self._id_column, _wrapped=False ) class has_many(relation): """ Has Many relationship decorator """ relation_class = HasMany def __init__(self, foreign_key=None, local_key=None, relation=None): if isinstance(foreign_key, (types.FunctionType, types.MethodType)): func = foreign_key foreign_key = None else: func = None self._foreign_key = foreign_key self._local_key = local_key super(has_many, self).__init__(func, relation) def _get(self, instance): return instance.has_many( self._related, self._foreign_key, self._local_key, self._relation, _wrapped=False ) class has_many_through(relation): """ Has Many Through relationship decorator """ relation_class = HasManyThrough def __init__(self, through, first_key=None, second_key=None, relation=None): if isinstance(through, (types.FunctionType, types.MethodType)): raise RuntimeError('has_many_through relation requires the through parameter') self._through = through self._first_key = first_key self._second_key = second_key super(has_many_through, self).__init__(relation=relation) def _get(self, instance): return instance.has_many_through( self._related, self._through, self._first_key, self._second_key, self._relation, _wrapped=False ) class morph_many(relation): """ Morph Many relationship decorator """ relation_class = MorphMany def __init__(self, name, type_column=None, id_column=None, local_key=None, relation=None): if isinstance(name, (types.FunctionType, types.MethodType)): raise RuntimeError('morph_many relation requires a name') self._name = name self._type_column = type_column self._id_column = id_column self._local_key = local_key super(morph_many, self).__init__(relation=relation) def _get(self, instance): return instance.morph_many( self._related, self._name, self._type_column, self._id_column, self._local_key, self._relation, _wrapped=False ) class belongs_to_many(relation): """ Belongs To Many relationship decorator """ relation_class = BelongsToMany def __init__(self, table=None, foreign_key=None, other_key=None, relation=None, with_timestamps=False, with_pivot=None): if isinstance(table, (types.FunctionType, types.MethodType)): func = table table = None else: func = None self._table = table self._foreign_key = foreign_key self._other_key = other_key self._timestamps = with_timestamps self._pivot = with_pivot super(belongs_to_many, self).__init__(func, relation) def _get(self, instance): r = instance.belongs_to_many( self._related, self._table, self._foreign_key, self._other_key, self._relation, _wrapped=False ) if self._timestamps: r = r.with_timestamps() if self._pivot: r = r.with_pivot(*self._pivot) return r class morph_to_many(relation): """ Morph To Many relationship decorator """ relation_class = MorphToMany def __init__(self, name, table=None, foreign_key=None, other_key=None, relation=None): if isinstance(name, (types.FunctionType, types.MethodType)): raise RuntimeError('morph_to_many relation required a name') self._name = name self._table = table self._foreign_key = foreign_key self._other_key = other_key super(morph_to_many, self).__init__(relation=relation) def _get(self, instance): return instance.morph_to_many( self._related, self._name, self._table, self._foreign_key, self._other_key, relation=self._relation, _wrapped=False ) class morphed_by_many(relation): """ Morphed By Many relationship decorator """ relation_class = MorphToMany def __init__(self, name, table=None, foreign_key=None, other_key=None, relation=None): if isinstance(foreign_key, (types.FunctionType, types.MethodType)): raise RuntimeError('morphed_by_many relation requires a name') self._name = name self._table = table self._foreign_key = foreign_key self._other_key = other_key super(morphed_by_many, self).__init__(relation=relation) def _get(self, instance): return instance.morphed_by_many( self._related, self._name, self._table, self._foreign_key, self._other_key, self._relation, _wrapped=False ) PK!7:ssorator/pagination/__init__.py# -*- coding: utf-8 -*- from .paginator import Paginator from .length_aware_paginator import LengthAwarePaginator PK!$orator/pagination/base.py# -*- coding: utf-8 -*- class BasePaginator(object): _current_page_resolver = None def _is_valid_page_number(self, page): """ Determine if the given value is a valid page number. :param page: The given page number :type page: int :rtype: bool """ return isinstance(page, int) and page >= 1 @property def items(self): """ Get the slice of items being paginated. :rtype: list """ return self._items.all() @property def first_item(self): """ Get the number of the first item in the slice. :rtype: int """ return (self.current_page - 1) * self.per_page + 1 @property def last_item(self): """ Get the number of the last item in the slice. :rtype: int """ return self.first_item + self.count() - 1 def has_pages(self): """ Determine if there are enough items to split into multiple pages. :rtype: int """ return not (self.current_page == 1 and not self.has_more_pages()) def is_empty(self): """ Determine if the list of items is empty or not. :rtype: bool """ return self._items.is_empty() def count(self): """ Get the number of items for the current page. :rtype: int """ return len(self._items) @property def previous_page(self): if self.current_page > 1: return self.current_page - 1 @property def next_page(self): if self.has_more_pages(): return self.current_page + 1 def get_collection(self): return self._items @classmethod def resolve_current_page(cls, default=1): if cls._current_page_resolver is not None: return cls._current_page_resolver() return default @classmethod def current_page_resolver(cls, resolver): cls._current_page_resolver = staticmethod(resolver) def __len__(self): return self.count() def __iter__(self): for item in self._items: yield item def __getitem__(self, item): return self.items[item] PK!jR_6{ { +orator/pagination/length_aware_paginator.py# -*- coding: utf-8 -*- from __future__ import division import math from .base import BasePaginator from ..support.collection import Collection from ..utils import deprecated class LengthAwarePaginator(BasePaginator): def __init__(self, items, total, per_page, current_page=None, options=None): """ Constructor :param items: The items being paginated :type items: mixed :param total: Total number of results :type total: int :param per_page: The number of results per page :type per_page: int :param current_page: The current page of results :type current_page: int :param options: Extra options to set :type options: dict """ if options is not None: for key, value in options.items(): setattr(self, key, value) self.total = total self.per_page = per_page self.last_page = int(math.ceil(total / per_page)) self.current_page = self._set_current_page(current_page, self.last_page) if isinstance(items, Collection): self._items = items else: self._items = Collection.make(items) def _set_current_page(self, current_page, last_page): """ Get the current page for the request. :param current_page: The current page of results :type current_page: int :param last_page: The last page of results :type last_page: int :rtype: int """ if not current_page: current_page = self.resolve_current_page() if current_page > last_page: if last_page > 0: return last_page return 1 if not self._is_valid_page_number(current_page): return 1 return current_page def has_more_pages(self): """ Determine if there are more items in the data source. :rtype: int """ return self.current_page < self.last_page @deprecated def to_dict(self): """ Alias for serialize. :rtype: list """ return self.serialize() def serialize(self): """ Convert the object into something JSON serializable. :rtype: list """ return self._items.serialize() def to_json(self, **options): return self._items.to_json(**options) PK!"orator/pagination/paginator.py# -*- coding: utf-8 -*- from .base import BasePaginator from ..support.collection import Collection from ..utils import deprecated class Paginator(BasePaginator): def __init__(self, items, per_page, current_page=None, options=None): """ Constructor :param items: The items being paginated :type items: mixed :param per_page: The number of results per page :type per_page: int :param current_page: The current page of results :type current_page: int :param options: Extra options to set :type options: dict """ if options is not None: for key, value in options.items(): setattr(self, key, value) self.per_page = per_page self.current_page = self._set_current_page(current_page) if isinstance(items, Collection): self._items = items else: self._items = Collection.make(items) self._check_for_more_pages() def _set_current_page(self, current_page): """ Get the current page for the request. :param current_page: The current page of results :type current_page: int :rtype: int """ if not current_page: self.resolve_current_page() if not self._is_valid_page_number(current_page): return 1 return current_page def _check_for_more_pages(self): """ Check for more pages. The last item will be sliced off. """ self._has_more = len(self._items) > self.per_page self._items = self._items[0:self.per_page] def has_more_pages(self): """ Determine if there are more items in the data source. :rtype: int """ return self._has_more @deprecated def to_dict(self): """ Alias for serialize. :rtype: list """ return self.serialize() def serialize(self): """ Convert the object into something JSON serializable. :rtype: list """ return self._items.serialize() def to_json(self, **options): return self._items.to_json(**options) PK!ȃ;;orator/query/__init__.py# -*- coding: utf-8 -*- from .builder import QueryBuilder PK!?(êêorator/query/builder.py# -*- coding: utf-8 -*- import re import copy import datetime from itertools import chain from collections import OrderedDict from .expression import QueryExpression from .join_clause import JoinClause from ..pagination import Paginator, LengthAwarePaginator from ..utils import basestring, Null from ..exceptions import ArgumentError from ..support import Collection class QueryBuilder(object): _operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', 'like', 'like binary', 'not like', 'between', 'ilike', '&', '|', '^', '<<', '>>', 'rlike', 'regexp', 'not regexp', '~', '~*', '!~', '!~*', 'similar to', 'not similar to', ] def __init__(self, connection, grammar, processor): """ Constructor :param connection: A Connection instance :type connection: Connection :param grammar: A QueryGrammar instance :type grammar: QueryGrammar :param processor: A QueryProcessor instance :type processor: QueryProcessor """ self._grammar = grammar self._processor = processor self._connection = connection self._bindings = OrderedDict() for type in ['select', 'join', 'where', 'having', 'order']: self._bindings[type] = [] self.aggregate_ = None self.columns = [] self.distinct_ = False self.from__ = '' self.joins = [] self.wheres = [] self.groups = [] self.havings = [] self.orders = [] self.limit_ = None self.offset_ = None self.unions = [] self.union_limit = None self.union_offset = None self.union_orders = [] self.lock_ = None self._backups = {} self._use_write_connection = False def select(self, *columns): """ Set the columns to be selected :param columns: The columns to be selected :type columns: tuple :return: The current QueryBuilder instance :rtype: QueryBuilder """ if not columns: columns = ['*'] self.columns = list(columns) return self def select_raw(self, expression, bindings=None): """ Add a new raw select expression to the query :param expression: The raw expression :type expression: str :param bindings: The expression bindings :type bindings: list :return: The current QueryBuilder instance :rtype: QueryBuilder """ self.add_select(QueryExpression(expression)) if bindings: self.add_binding(bindings, 'select') return self def select_sub(self, query, as_): """ Add a subselect expression to the query :param query: A QueryBuilder instance :type query: QueryBuilder :param as_: The subselect alias :type as_: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ if isinstance(query, QueryBuilder): bindings = query.get_bindings() query = query.to_sql() elif isinstance(query, basestring): bindings = [] else: raise ArgumentError('Invalid subselect') return self.select_raw('(%s) AS %s' % (query, self._grammar.wrap(as_)), bindings) def add_select(self, *column): """ Add a new select column to query :param column: The column to add :type column: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ if not column: column = [] self.columns += list(column) return self def distinct(self): """ Force the query to return only distinct result :return: The current QueryBuilder instance :rtype: QueryBuilder """ self.distinct_ = True return self def from_(self, table): """ Set the query target table :param table: The table to target :type table: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ self.from__ = table return self def join(self, table, one=None, operator=None, two=None, type='inner', where=False): """ Add a join clause to the query :param table: The table to join with, can also be a JoinClause instance :type table: str or JoinClause :param one: The first column of the join condition :type one: str :param operator: The operator of the join condition :type operator: str :param two: The second column of the join condition :type two: str :param type: The join type :type type: str :param where: Whether to use a "where" rather than a "on" :type where: bool :return: The current QueryBuilder instance :rtype: QueryBuilder """ if isinstance(table, JoinClause): self.joins.append(table) else: if one is None: raise ArgumentError('Missing "one" argument') join = JoinClause(table, type) self.joins.append(join.on( one, operator, two, 'and', where )) return self def join_where(self, table, one, operator, two, type='inner'): """ Add a "join where" clause to the query :param table: The table to join with, can also be a JoinClause instance :type table: str or JoinClause :param one: The first column of the join condition :type one: str :param operator: The operator of the join condition :type operator: str :param two: The second column of the join condition :type two: str :param type: The join type :type type: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.join(table, one, operator, two, type, True) def left_join(self, table, one=None, operator=None, two=None): """ Add a left join to the query :param table: The table to join with, can also be a JoinClause instance :type table: str or JoinClause :param one: The first column of the join condition :type one: str :param operator: The operator of the join condition :type operator: str :param two: The second column of the join condition :type two: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ if isinstance(table, JoinClause): table.type = 'left' return self.join(table, one, operator, two, 'left') def left_join_where(self, table, one, operator, two): """ Add a "left join where" clause to the query :param table: The table to join with, can also be a JoinClause instance :type table: str or JoinClause :param one: The first column of the join condition :type one: str :param operator: The operator of the join condition :type operator: str :param two: The second column of the join condition :type two: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.join_where(table, one, operator, two, 'left') def right_join(self, table, one=None, operator=None, two=None): """ Add a right join to the query :param table: The table to join with, can also be a JoinClause instance :type table: str or JoinClause :param one: The first column of the join condition :type one: str :param operator: The operator of the join condition :type operator: str :param two: The second column of the join condition :type two: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ if isinstance(table, JoinClause): table.type = 'right' return self.join(table, one, operator, two, 'right') def right_join_where(self, table, one, operator, two): """ Add a "right join where" clause to the query :param table: The table to join with, can also be a JoinClause instance :type table: str or JoinClause :param one: The first column of the join condition :type one: str :param operator: The operator of the join condition :type operator: str :param two: The second column of the join condition :type two: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.join_where(table, one, operator, two, 'right') def where(self, column, operator=Null(), value=None, boolean='and'): """ Add a where clause to the query :param column: The column of the where clause, can also be a QueryBuilder instance for sub where :type column: str or QueryBuilder :param operator: The operator of the where clause :type operator: str :param value: The value of the where clause :type value: mixed :param boolean: The boolean of the where clause :type boolean: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ # If the column is an array, we will assume it is an array of key-value pairs # and can add them each as a where clause. We will maintain the boolean we # received when the method was called and pass it into the nested where. if isinstance(column, dict): nested = self.new_query() for key, value in column.items(): nested.where(key, '=', value) return self.where_nested(nested, boolean) if isinstance(column, QueryBuilder): return self.where_nested(column, boolean) if isinstance(column, list): nested = self.new_query() for condition in column: if isinstance(condition, list) and len(condition) == 3: nested.where(condition[0], condition[1], condition[2]) else: raise ArgumentError('Invalid conditions in where() clause') return self.where_nested(nested, boolean) if value is None: if not isinstance(operator, Null): value = operator operator = '=' else: raise ArgumentError('Value must be provided') if operator not in self._operators: value = operator operator = '=' if isinstance(value, QueryBuilder): return self._where_sub(column, operator, value, boolean) if value is None: return self.where_null(column, boolean, operator != '=') type = 'basic' self.wheres.append({ 'type': type, 'column': column, 'operator': operator, 'value': value, 'boolean': boolean }) if not isinstance(value, QueryExpression): self.add_binding(value, 'where') return self def or_where(self, column, operator=None, value=None): return self.where(column, operator, value, 'or') def _invalid_operator_and_value(self, operator, value): is_operator = operator in self._operators return is_operator and operator != '=' and value is None def where_raw(self, sql, bindings=None, boolean='and'): type = 'raw' self.wheres.append({ 'type': type, 'sql': sql, 'boolean': boolean }) self.add_binding(bindings, 'where') return self def or_where_raw(self, sql, bindings=None): return self.where_raw(sql, bindings, 'or') def where_between(self, column, values, boolean='and', negate=False): type = 'between' self.wheres.append({ 'column': column, 'type': type, 'boolean': boolean, 'not': negate }) self.add_binding(values, 'where') return self def or_where_between(self, column, values): return self.where_between(column, values, 'or') def where_not_between(self, column, values, boolean='and'): return self.where_between(column, values, boolean, True) def or_where_not_between(self, column, values): return self.where_not_between(column, values, 'or') def where_nested(self, query, boolean='and'): query.from_(self.from__) return self.add_nested_where_query(query, boolean) def for_nested_where(self): """ Create a new query instance for nested where condition. :rtype: QueryBuilder """ query = self.new_query() return query.from_(self.from__) def add_nested_where_query(self, query, boolean='and'): if len(query.wheres): type = 'nested' self.wheres.append({ 'type': type, 'query': query, 'boolean': boolean }) self.merge_bindings(query) return self def _where_sub(self, column, operator, query, boolean): type = 'sub' self.wheres.append({ 'type': type, 'column': column, 'operator': operator, 'query': query, 'boolean': boolean }) self.merge_bindings(query) return self def where_exists(self, query, boolean='and', negate=False): """ Add an exists clause to the query. :param query: The exists query :type query: QueryBuilder :type boolean: str :type negate: bool :rtype: QueryBuilder """ if negate: type = 'not_exists' else: type = 'exists' self.wheres.append({ 'type': type, 'query': query, 'boolean': boolean }) self.merge_bindings(query) return self def or_where_exists(self, query, negate=False): """ Add an or exists clause to the query. :param query: The exists query :type query: QueryBuilder :type negate: bool :rtype: QueryBuilder """ return self.where_exists(query, 'or', negate) def where_not_exists(self, query, boolean='and'): """ Add a where not exists clause to the query. :param query: The exists query :type query: QueryBuilder :type boolean: str :rtype: QueryBuilder """ return self.where_exists(query, boolean, True) def or_where_not_exists(self, query): """ Add a or where not exists clause to the query. :param query: The exists query :type query: QueryBuilder :rtype: QueryBuilder """ return self.or_where_exists(query, True) def where_in(self, column, values, boolean='and', negate=False): if negate: type = 'not_in' else: type = 'in' if isinstance(values, QueryBuilder): return self._where_in_sub(column, values, boolean, negate) if isinstance(values, Collection): values = values.all() self.wheres.append({ 'type': type, 'column': column, 'values': values, 'boolean': boolean }) self.add_binding(values, 'where') return self def or_where_in(self, column, values): return self.where_in(column, values, 'or') def where_not_in(self, column, values, boolean='and'): return self.where_in(column, values, boolean, True) def or_where_not_in(self, column, values): return self.where_not_in(column, values, 'or') def _where_in_sub(self, column, query, boolean, negate=False): """ Add a where in with a sub select to the query :param column: The column :type column: str :param query: A QueryBuilder instance :type query: QueryBuilder :param boolean: The boolean operator :type boolean: str :param negate: Whether it is a not where in :param negate: bool :return: The current QueryBuilder instance :rtype: QueryBuilder """ if negate: type = 'not_in_sub' else: type = 'in_sub' self.wheres.append({ 'type': type, 'column': column, 'query': query, 'boolean': boolean }) self.merge_bindings(query) return self def where_null(self, column, boolean='and', negate=False): if negate: type = 'not_null' else: type = 'null' self.wheres.append({ 'type': type, 'column': column, 'boolean': boolean }) return self def or_where_null(self, column): return self.where_null(column, 'or') def where_not_null(self, column, boolean='and'): return self.where_null(column, boolean, True) def or_where_not_null(self, column): return self.where_not_null(column, 'or') def where_date(self, column, operator, value, boolean='and'): return self._add_date_based_where('date', column, operator, value, boolean) def where_day(self, column, operator, value, boolean='and'): return self._add_date_based_where('day', column, operator, value, boolean) def where_month(self, column, operator, value, boolean='and'): return self._add_date_based_where('month', column, operator, value, boolean) def where_year(self, column, operator, value, boolean='and'): return self._add_date_based_where('year', column, operator, value, boolean) def _add_date_based_where(self, type, column, operator, value, boolean='and'): self.wheres.append({ 'type': type, 'column': column, 'boolean': boolean, 'operator': operator, 'value': value }) self.add_binding(value, 'where') def dynamic_where(self, method): finder = method[6:] def dynamic_where(*parameters): segments = re.split('_(and|or)_(?=[a-z])', finder, 0, re.I) connector = 'and' index = 0 for segment in segments: if segment.lower() != 'and' and segment.lower() != 'or': self._add_dynamic(segment, connector, parameters, index) index += 1 else: connector = segment return self return dynamic_where def _add_dynamic(self, segment, connector, parameters, index): self.where(segment, '=', parameters[index], connector) def group_by(self, *columns): """ Add a "group by" clause to the query :param columns: The columns to group by :type columns: tuple :return: The current QueryBuilder instance :rtype: QueryBuilder """ for column in columns: self.groups.append(column) return self def having(self, column, operator=None, value=None, boolean='and'): """ Add a "having" clause to the query :param column: The column :type column: str :param operator: The having clause operator :type operator: str :param value: The having clause value :type value: mixed :param boolean: Boolean joiner type :type boolean: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ type = 'basic' self.havings.append({ 'type': type, 'column': column, 'operator': operator, 'value': value, 'boolean': boolean }) if not isinstance(value, QueryExpression): self.add_binding(value, 'having') return self def or_having(self, column, operator=None, value=None): """ Add a "having" clause to the query :param column: The column :type column: str :param operator: The having clause operator :type operator: str :param value: The having clause value :type value: mixed :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.having(column, operator, value, 'or') def having_raw(self, sql, bindings=None, boolean='and'): """ Add a raw having clause to the query :param sql: The raw query :type sql: str :param bindings: The query bindings :type bindings: list :param boolean: Boolean joiner type :type boolean: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ type = 'raw' self.havings.append({ 'type': type, 'sql': sql, 'boolean': boolean }) self.add_binding(bindings, 'having') return self def or_having_raw(self, sql, bindings=None): """ Add a raw having clause to the query :param sql: The raw query :type sql: str :param bindings: The query bindings :type bindings: list :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.having_raw(sql, bindings, 'or') def order_by(self, column, direction='asc'): """ Add a "order by" clause to the query :param column: The order by column :type column: str :param direction: The direction of the order :type direction: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ if self.unions: prop = 'union_orders' else: prop = 'orders' if direction.lower() == 'asc': direction = 'asc' else: direction = 'desc' getattr(self, prop).append({ 'column': column, 'direction': direction }) return self def latest(self, column='created_at'): """ Add an "order by" clause for a timestamp to the query in descending order :param column: The order by column :type column: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.order_by(column, 'desc') def oldest(self, column='created_at'): """ Add an "order by" clause for a timestamp to the query in ascending order :param column: The order by column :type column: str :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.order_by(column, 'asc') def order_by_raw(self, sql, bindings=None): """ Add a raw "order by" clause to the query :param sql: The raw clause :type sql: str :param bindings: The bdings :param bindings: list :return: The current QueryBuilder instance :rtype: QueryBuilder """ if bindings is None: bindings = [] type = 'raw' self.orders.append({ 'type': type, 'sql': sql }) self.add_binding(bindings, 'order') return self def offset(self, value): if self.unions: prop = 'union_offset' else: prop = 'offset_' setattr(self, prop, max(0, value)) return self def skip(self, value): return self.offset(value) def limit(self, value): if self.unions: prop = 'union_limit' else: prop = 'limit_' if value is None or value > 0: setattr(self, prop, value) return self def take(self, value): return self.limit(value) def for_page(self, page, per_page=15): return self.skip((page - 1) * per_page).take(per_page) def union(self, query, all=False): """ Add a union statement to the query :param query: A QueryBuilder instance :type query: QueryBuilder :param all: Whether it is a "union all" statement :type all: bool :return: The query :rtype: QueryBuilder """ self.unions.append({ 'query': query, 'all': all }) return self.merge_bindings(query) def union_all(self, query): """ Add a union all statement to the query :param query: A QueryBuilder instance :type query: QueryBuilder :return: The query :rtype: QueryBuilder """ return self.union(query, True) def lock(self, value=True): """ Lock the selected rows in the table :param value: Whether it is a lock for update or a shared lock :type value: bool :return: The current QueryBuilder instance :rtype: QueryBuilder """ self.lock_ = value return self def lock_for_update(self): """ Lock the selected rows in the table for updating. :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.lock(True) def shared_lock(self): """ Share lock the selected rows in the table. :return: The current QueryBuilder instance :rtype: QueryBuilder """ return self.lock(False) def to_sql(self): """ Get the SQL representation of the query :return: The SQL representation of the query :rtype: str """ return self._grammar.compile_select(self) def find(self, id, columns=None): """ Execute a query for a single record by id :param id: The id of the record to retrieve :type id: mixed :param columns: The columns of the record to retrive :type columns: list :return: mixed :rtype: mixed """ if not columns: columns = ['*'] return self.where('id', '=', id).first(1, columns) def pluck(self, column): """ Pluck a single column's value from the first results of a query :param column: The column to pluck the value from :type column: str :return: The value of column :rtype: mixed """ result = self.first(1, [column]) if result: return result[column] return def first(self, limit=1, columns=None): """ Execute the query and get the first results :param limit: The number of results to get :type limit: int :param columns: The columns to get :type columns: list :return: The result :rtype: mixed """ if not columns: columns = ['*'] return self.take(limit).get(columns).first() def get(self, columns=None): """ Execute the query as a "select" statement :param columns: The columns to get :type columns: list :return: The result :rtype: Collection """ if not columns: columns = ['*'] original = self.columns if not original: self.columns = columns results = self._processor.process_select(self, self._run_select()) self.columns = original return Collection(results) def _run_select(self): """ Run the query as a "select" statement against the connection. :return: The result :rtype: list """ return self._connection.select( self.to_sql(), self.get_bindings(), not self._use_write_connection ) def paginate(self, per_page=15, current_page=None, columns=None): """ Paginate the given query. :param per_page: The number of records per page :type per_page: int :param current_page: The current page of results :type current_page: int :param columns: The columns to return :type columns: list :return: The paginator :rtype: LengthAwarePaginator """ if columns is None: columns = ['*'] page = current_page or Paginator.resolve_current_page() total = self.get_count_for_pagination() results = self.for_page(page, per_page).get(columns) return LengthAwarePaginator(results, total, per_page, page) def simple_paginate(self, per_page=15, current_page=None, columns=None): """ Paginate the given query. :param per_page: The number of records per page :type per_page: int :param current_page: The current page of results :type current_page: int :param columns: The columns to return :type columns: list :return: The paginator :rtype: Paginator """ if columns is None: columns = ['*'] page = current_page or Paginator.resolve_current_page() self.skip((page - 1) * per_page).take(per_page + 1) return Paginator(self.get(columns), per_page, page) def get_count_for_pagination(self): self._backup_fields_for_count() total = self.count() self._restore_fields_for_count() return total def _backup_fields_for_count(self): for field in ['orders', 'limit', 'offset']: self._backups[field] = getattr(self, field) setattr(self, field, None) def _restore_fields_for_count(self): for field in ['orders', 'limit', 'offset']: setattr(self, field, self._backups[field]) self._backups = {} def chunk(self, count): """ Chunk the results of the query :param count: The chunk size :type count: int :return: The current chunk :rtype: list """ for chunk in self._connection.select_many( count, self.to_sql(), self.get_bindings(), not self._use_write_connection ): yield chunk def lists(self, column, key=None): """ Get a list with the values of a given column :param column: The column to get the values for :type column: str :param key: The key :type key: str :return: The list of values :rtype: Collection or dict """ columns = self._get_list_select(column, key) if key is not None: results = {} for result in self.get(columns): results[result[key]] = result[column] else: results = Collection(list(map(lambda x: x[column], self.get(columns)))) return results def _get_list_select(self, column, key=None): """ Get the columns that should be used in a list :param column: The column to get the values for :type column: str :param key: The key :type key: str :return: The list of values :rtype: list """ if key is None: elements = [column] else: elements = [column, key] select = [] for elem in elements: dot = elem.find('.') if dot >= 0: select.append(column[dot + 1:]) else: select.append(elem) return select def implode(self, column, glue=''): """ Concatenate values of a given column as a string. :param column: The column to glue the values for :type column: str :param glue: The glue string :type glue: str :return: The glued value :rtype: str """ return self.lists(column).implode(glue) def exists(self): """ Determine if any rows exist for the current query. :return: Whether the rows exist or not :rtype: bool """ limit = self.limit_ result = self.limit(1).count() > 0 self.limit(limit) return result def count(self, *columns): """ Retrieve the "count" result of the query :param columns: The columns to get :type columns: tuple :return: The count :rtype: int """ if not columns and self.distinct_: columns = self.columns if not columns: columns = ['*'] return int(self.aggregate('count', *columns)) def min(self, column): """ Retrieve the "min" result of the query :param column: The column to get the minimun for :type column: tuple :return: The min :rtype: int """ return self.aggregate('min', *[column]) def max(self, column): """ Retrieve the "max" result of the query :param column: The column to get the maximum for :type column: tuple :return: The max :rtype: int """ if not column: columns = ['*'] return self.aggregate('max', *[column]) def sum(self, column): """ Retrieve the "sum" result of the query :param column: The column to get the sum for :type column: tuple :return: The sum :rtype: int """ return self.aggregate('sum', *[column]) def avg(self, column): """ Retrieve the "avg" result of the query :param column: The column to get the average for :type column: tuple :return: The count :rtype: int """ return self.aggregate('avg', *[column]) def aggregate(self, func, *columns): """ Execute an aggregate function against the database :param func: The aggregate function :type func: str :param columns: The columns to execute the fnction for :type columns: tuple :return: The aggregate result :rtype: mixed """ if not columns: columns = ['*'] self.aggregate_ = { 'function': func, 'columns': columns } previous_columns = self.columns results = self.get(*columns).all() self.aggregate_ = None self.columns = previous_columns if len(results) > 0: return dict((k.lower(), v) for k, v in results[0].items())['aggregate'] def insert(self, _values=None, **values): """ Insert a new record into the database :param _values: The new record values :type _values: dict or list :param values: The new record values as keyword arguments :type values: dict :return: The result :rtype: bool """ if not values and not _values: return True if not isinstance(_values, list): if _values is not None: values.update(_values) values = [values] else: values = _values for i, value in enumerate(values): values[i] = OrderedDict(sorted(value.items())) bindings = [] for record in values: for value in record.values(): bindings.append(value) sql = self._grammar.compile_insert(self, values) bindings = self._clean_bindings(bindings) return self._connection.insert(sql, bindings) def insert_get_id(self, values, sequence=None): """ Insert a new record and get the value of the primary key :param values: The new record values :type values: dict :param sequence: The name of the primary key :type sequence: str :return: The value of the primary key :rtype: int """ values = OrderedDict(sorted(values.items())) sql = self._grammar.compile_insert_get_id(self, values, sequence) values = self._clean_bindings(values.values()) return self._processor.process_insert_get_id(self, sql, values, sequence) def update(self, _values=None, **values): """ Update a record in the database :param values: The values of the update :type values: dict :return: The number of records affected :rtype: int """ if _values is not None: values.update(_values) values = OrderedDict(sorted(values.items())) bindings = list(values.values()) + self.get_bindings() sql = self._grammar.compile_update(self, values) return self._connection.update(sql, self._clean_bindings(bindings)) def increment(self, column, amount=1, extras=None): """ Increment a column's value by a given amount :param column: The column to increment :type column: str :param amount: The amount by which to increment :type amount: int :param extras: Extra columns :type extras: dict :return: The number of rows affected :rtype: int """ wrapped = self._grammar.wrap(column) if extras is None: extras = {} columns = { column: self.raw('%s + %s' % (wrapped, amount)) } columns.update(extras) return self.update(**columns) def decrement(self, column, amount=1, extras=None): """ Decrement a column's value by a given amount :param column: The column to increment :type column: str :param amount: The amount by which to increment :type amount: int :param extras: Extra columns :type extras: dict :return: The number of rows affected :rtype: int """ wrapped = self._grammar.wrap(column) if extras is None: extras = {} columns = { column: self.raw('%s - %s' % (wrapped, amount)) } columns.update(extras) return self.update(**columns) def delete(self, id=None): """ Delete a record from the database :param id: The id of the row to delete :type id: mixed :return: The number of rows deleted :rtype: int """ if id is not None: self.where('id', '=', id) sql = self._grammar.compile_delete(self) return self._connection.delete(sql, self.get_bindings()) def truncate(self): """ Run a truncate statement on the table :rtype: None """ for sql, bindings in self._grammar.compile_truncate(self).items(): self._connection.statement(sql, bindings) def new_query(self): """ Get a new instance of the query builder :return: A new QueryBuilder instance :rtype: QueryBuilder """ return QueryBuilder(self._connection, self._grammar, self._processor) def merge_wheres(self, wheres, bindings): """ Merge a list of where clauses and bindings :param wheres: A list of where clauses :type wheres: list :param bindings: A list of bindings :type bindings: list :rtype: None """ self.wheres = self.wheres + wheres self._bindings['where'] = self._bindings['where'] + bindings def _clean_bindings(self, bindings): """ Remove all of the expressions from bindings :param bindings: The bindings to clean :type bindings: list :return: The cleaned bindings :rtype: list """ return list(filter(lambda b: not isinstance(b, QueryExpression), bindings)) def raw(self, value): """ Create a raw database expression :param value: The value of the raw expression :type value: mixed :return: A QueryExpression instance :rtype: QueryExpression """ return self._connection.raw(value) def get_bindings(self): bindings = [] for value in chain(*self._bindings.values()): if isinstance(value, datetime.date): value = value.strftime(self._grammar.get_date_format()) bindings.append(value) return bindings def get_raw_bindings(self): return self._bindings def set_bindings(self, bindings, type='where'): if type not in self._bindings: raise ArgumentError('Invalid binding type: %s' % type) self._bindings[type] = bindings return self def add_binding(self, value, type='where'): if value is None: return self if type not in self._bindings: raise ArgumentError('Invalid binding type: %s' % type) if isinstance(value, (list, tuple)): self._bindings[type] += value else: self._bindings[type].append(value) return self def merge_bindings(self, query): for type in self._bindings: self._bindings[type] += query.get_raw_bindings()[type] return self def merge(self, query): """ Merge current query with another. :param query: The query to merge with :type query: QueryBuilder """ self.columns += query.columns self.joins += query.joins self.wheres += query.wheres self.groups += query.groups self.havings += query.havings self.orders += query.orders self.distinct_ = query.distinct_ if self.columns: self.columns = Collection(self.columns).unique().all() if query.limit_: self.limit_ = query.limit_ if query.offset_: self.offset_ = None self.unions += query.unions if query.union_limit: self.union_limit = query.union_limit if query.union_offset: self.union_offset = query.union_offset self.union_orders += query.union_orders self.merge_bindings(query) def get_connection(self): """ Get the query connection :return: The current connection instance :rtype: orator.connections.connection.Connection """ return self._connection def get_processor(self): """ Get the builder processor :return: The builder processor :rtype: QueryProcessor """ return self._processor def get_grammar(self): """ Get the builder query grammar :return: The builder query grammar :rtype: QueryGrammar """ return self._grammar def use_write_connection(self): self._use_write_connection = True return self def __getattr__(self, item): if item.startswith('where_'): return self.dynamic_where(item) raise AttributeError(item) def __copy__(self): new = self.__class__(self._connection, self._grammar, self._processor) new.__dict__.update(dict((k, copy.deepcopy(v)) for k, v in self.__dict__.items() if k != '_connection')) return new PK!幋orator/query/expression.py# -*- coding: utf-8 -*- class QueryExpression(object): def __init__(self, value): self._value = value def get_value(self): return self._value def __str__(self): return str(self.get_value()) PK![,!orator/query/grammars/__init__.py# -*- coding: utf-8 -*- from .grammar import QueryGrammar from .postgres_grammar import PostgresQueryGrammar from .mysql_grammar import MySQLQueryGrammar from .sqlite_grammar import SQLiteQueryGrammar PK!H`66 orator/query/grammars/grammar.py# -*- coding: utf-8 -*- import re from ...support.grammar import Grammar from ..builder import QueryBuilder from ...utils import basestring class QueryGrammar(Grammar): _select_components = [ 'aggregate_', 'columns', 'from__', 'joins', 'wheres', 'groups', 'havings', 'orders', 'limit_', 'offset_', 'unions', 'lock_' ] def compile_select(self, query): if not query.columns: query.columns = ['*'] return self._concatenate(self._compile_components(query)).strip() def _compile_components(self, query): sql = {} for component in self._select_components: # To compile the query, we'll spin through each component of the query and # see if that component exists. If it does we'll just call the compiler # function for the component which is responsible for making the SQL. component_value = getattr(query, component) if component_value is not None: method = '_compile_%s' % component.replace('_', '') sql[component] = getattr(self, method)(query, component_value) return sql def _compile_aggregate(self, query, aggregate): column = self.columnize(aggregate['columns']) if query.distinct_ and column != '*': column = 'DISTINCT %s' % column return 'SELECT %s(%s) AS aggregate' % (aggregate['function'].upper(), column) def _compile_columns(self, query, columns): # If the query is actually performing an aggregating select, we will let that # compiler handle the building of the select clauses, as it will need some # more syntax that is best handled by that function to keep things neat. if query.aggregate_ is not None: return if query.distinct_: select = 'SELECT DISTINCT ' else: select = 'SELECT ' return '%s%s' % (select, self.columnize(columns)) def _compile_from(self, query, table): return 'FROM %s' % self.wrap_table(table) def _compile_joins(self, query, joins): sql = [] query.set_bindings([], 'join') for join in joins: table = self.wrap_table(join.table) # First we need to build all of the "on" clauses for the join. There may be many # of these clauses so we will need to iterate through each one and build them # separately, then we'll join them up into a single string when we're done. clauses = [] for clause in join.clauses: clauses.append(self._compile_join_constraints(clause)) for binding in join.bindings: query.add_binding(binding, 'join') # Once we have constructed the clauses, we'll need to take the boolean connector # off of the first clause as it obviously will not be required on that clause # because it leads the rest of the clauses, thus not requiring any boolean. clauses[0] = self._remove_leading_boolean(clauses[0]) clauses = ' '.join(clauses) type = join.type # Once we have everything ready to go, we will just concatenate all the parts to # build the final join statement SQL for the query and we can then return the # final clause back to the callers as a single, stringified join statement. sql.append('%s JOIN %s ON %s' % (type.upper(), table, clauses)) return ' '.join(sql) def _compile_join_constraints(self, clause): first = self.wrap(clause['first']) if clause['where']: second = self.get_marker() else: second = self.wrap(clause['second']) return '%s %s %s %s' % (clause['boolean'].upper(), first, clause['operator'], second) def _compile_wheres(self, query, _=None): sql = [] if query.wheres is None: return '' # Each type of where clauses has its own compiler function which is responsible # for actually creating the where clauses SQL. This helps keep the code nice # and maintainable since each clause has a very small method that it uses. for where in query.wheres: method = '_where_%s' % where['type'] sql.append('%s %s' % (where['boolean'].upper(), getattr(self, method)(query, where))) # If we actually have some where clauses, we will strip off the first boolean # operator, which is added by the query builders for convenience so we can # avoid checking for the first clauses in each of the compilers methods. if len(sql) > 0: sql = ' '.join(sql) return 'WHERE %s' % re.sub('AND |OR ', '', sql, 1, re.I) return '' def _where_nested(self, query, where): nested = where['query'] return '(%s)' % (self._compile_wheres(nested)[6:]) def _where_sub(self, query, where): select = self.compile_select(where['query']) return '%s %s (%s)' % (self.wrap(where['column']), where['operator'], select) def _where_basic(self, query, where): value = self.parameter(where['value']) return '%s %s %s' % (self.wrap(where['column']), where['operator'], value) def _where_between(self, query, where): if where['not']: between = 'NOT BETWEEN' else: between = 'BETWEEN' return '%s %s %s AND %s' % (self.wrap(where['column']), between, self.get_marker(), self.get_marker()) def _where_exists(self, query, where): return 'EXISTS (%s)' % self.compile_select(where['query']) def _where_not_exists(self, query, where): return 'NOT EXISTS (%s)' % self.compile_select(where['query']) def _where_in(self, query, where): if not where['values']: return '0 = 1' values = self.parameterize(where['values']) return '%s IN (%s)' % (self.wrap(where['column']), values) def _where_not_in(self, query, where): if not where['values']: return '1 = 1' values = self.parameterize(where['values']) return '%s NOT IN (%s)' % (self.wrap(where['column']), values) def _where_in_sub(self, query, where): select = self.compile_select(where['query']) return '%s IN (%s)' % (self.wrap(where['column']), select) def _where_not_in_sub(self, query, where): select = self.compile_select(where['query']) return '%s NOT IN (%s)' % (self.wrap(where['column']), select) def _where_null(self, query, where): return '%s IS NULL' % self.wrap(where['column']) def _where_not_null(self, query, where): return '%s IS NOT NULL' % self.wrap(where['column']) def _where_date(self, query, where): return self._date_based_where('date', query, where) def _where_day(self, query, where): return self._date_based_where('day', query, where) def _where_month(self, query, where): return self._date_based_where('month', query, where) def _where_year(self, query, where): return self._date_based_where('year', query, where) def _date_based_where(self, type, query, where): value = self.parameter(where['value']) return '%s(%s) %s %s' % (type.upper(), self.wrap(where['column']), where['operator'], value) def _where_raw(self, query, where): return re.sub('( and | or )', lambda m: m.group(1).upper(), where['sql'], re.I) def _compile_groups(self, query, groups): if not groups: return '' return 'GROUP BY %s' % self.columnize(groups) def _compile_havings(self, query, havings): if not havings: return '' sql = ' '.join(map(self._compile_having, havings)) return 'HAVING %s' % re.sub('and |or ', '', sql, 1, re.I) def _compile_having(self, having): # If the having clause is "raw", we can just return the clause straight away # without doing any more processing on it. Otherwise, we will compile the # clause into SQL based on the components that make it up from builder. if having['type'] == 'raw': return '%s %s' % (having['boolean'].upper(), having['sql']) return self._compile_basic_having(having) def _compile_basic_having(self, having): column = self.wrap(having['column']) parameter = self.parameter(having['value']) return '%s %s %s %s' % (having['boolean'].upper(), column, having['operator'], parameter) def _compile_orders(self, query, orders): if not orders: return '' compiled = [] for order in orders: if order.get('sql'): compiled.append(re.sub('( desc| asc)( |$)', lambda m: '%s%s' % (m.group(1).upper(), m.group(2)), order['sql'], re.I)) else: compiled.append('%s %s' % (self.wrap(order['column']), order['direction'].upper())) return 'ORDER BY %s' % ', '.join(compiled) def _compile_limit(self, query, limit): return 'LIMIT %s' % int(limit) def _compile_offset(self, query, offset): return 'OFFSET %s' % int(offset) def _compile_unions(self, query, _=None): sql = '' for union in query.unions: sql += self._compile_union(union) if query.union_orders: sql += ' %s' % self._compile_orders(query, query.union_orders) if query.union_limit: sql += ' %s' % self._compile_limit(query, query.union_limit) if query.union_offset: sql += ' %s' % self._compile_offset(query, query.union_offset) return sql.lstrip() def _compile_union(self, union): if union['all']: joiner = ' UNION ALL ' else: joiner = ' UNION ' return '%s%s' % (joiner, union['query'].to_sql()) def compile_insert(self, query, values): """ Compile an insert SQL statement :param query: A QueryBuilder instance :type query: QueryBuilder :param values: The values to insert :type values: dict or list :return: The compiled statement :rtype: str """ # Essentially we will force every insert to be treated as a batch insert which # simply makes creating the SQL easier for us since we can utilize the same # basic routine regardless of an amount of records given to us to insert. table = self.wrap_table(query.from__) if not isinstance(values, list): values = [values] columns = self.columnize(values[0].keys()) # We need to build a list of parameter place-holders of values that are bound # to the query. Each insert should have the exact same amount of parameter # bindings so we can just go off the first list of values in this array. parameters = self.parameterize(values[0].values()) value = ['(%s)' % parameters] * len(values) parameters = ', '.join(value) return 'INSERT INTO %s (%s) VALUES %s' % (table, columns, parameters) def compile_insert_get_id(self, query, values, sequence): return self.compile_insert(query, values) def compile_update(self, query, values): table = self.wrap_table(query.from__) # Each one of the columns in the update statements needs to be wrapped in the # keyword identifiers, also a place-holder needs to be created for each of # the values in the list of bindings so we can make the sets statements. columns = [] for key, value in values.items(): columns.append('%s = %s' % (self.wrap(key), self.parameter(value))) columns = ', '.join(columns) # If the query has any "join" clauses, we will setup the joins on the builder # and compile them so we can attach them to this update, as update queries # can get join statements to attach to other tables when they're needed. if query.joins: joins = ' %s' % self._compile_joins(query, query.joins) else: joins = '' # Of course, update queries may also be constrained by where clauses so we'll # need to compile the where clauses and attach it to the query so only the # intended records are updated by the SQL statements we generate to run. where = self._compile_wheres(query) return ('UPDATE %s%s SET %s %s' % (table, joins, columns, where)).strip() def compile_delete(self, query): table = self.wrap_table(query.from__) if isinstance(query.wheres, list): where = self._compile_wheres(query) else: where = '' return ('DELETE FROM %s %s' % (table, where)).strip() def compile_truncate(self, query): return { 'TRUNCATE %s' % self.wrap_table(query.from__): [] } def _compile_lock(self, query, value): if isinstance(value, basestring): return value else: return '' def _concatenate(self, segments): parts = [] for component in self._select_components: value = segments.get(component) if value: parts.append(value) return ' '.join(parts) def _remove_leading_boolean(self, value): return re.sub('and | or ', '', value, 1, re.I) PK!xO &orator/query/grammars/mysql_grammar.py# -*- coding: utf-8 -*- from .grammar import QueryGrammar from ...utils import basestring class MySQLQueryGrammar(QueryGrammar): _select_components = [ 'aggregate_', 'columns', 'from__', 'joins', 'wheres', 'groups', 'havings', 'orders', 'limit_', 'offset_', 'lock_' ] marker = '%s' def compile_select(self, query): """ Compile a select query into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled sql :rtype: str """ sql = super(MySQLQueryGrammar, self).compile_select(query) if query.unions: sql = '(%s) %s' % (sql, self._compile_unions(query)) return sql def _compile_union(self, union): """ Compile a single union statement :param union: The union statement :type union: dict :return: The compiled union statement :rtype: str """ if union['all']: joiner = ' UNION ALL ' else: joiner = ' UNION ' return '%s(%s)' % (joiner, union['query'].to_sql()) def _compile_lock(self, query, value): """ Compile the lock into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :param value: The lock value :type value: bool or str :return: The compiled lock :rtype: str """ if isinstance(value, basestring): return value if value is True: return 'FOR UPDATE' elif value is False: return 'LOCK IN SHARE MODE' def compile_update(self, query, values): """ Compile an update statement into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :param values: The update values :type values: dict :return: The compiled update :rtype: str """ sql = super(MySQLQueryGrammar, self).compile_update(query, values) if query.orders: sql += ' %s' % self._compile_orders(query, query.orders) if query.limit_: sql += ' %s' % self._compile_limit(query, query.limit_) return sql.rstrip() def compile_delete(self, query): """ Compile a delete statement into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled update :rtype: str """ table = self.wrap_table(query.from__) if isinstance(query.wheres, list): wheres = self._compile_wheres(query) else: wheres = '' if query.joins: joins = ' %s' % self._compile_joins(query, query.joins) sql = 'DELETE %s FROM %s%s %s' % (table, table, joins, wheres) else: sql = 'DELETE FROM %s %s' % (table, wheres) sql = sql.strip() if query.orders: sql += ' %s' % self._compile_orders(query, query.orders) if query.limit_: sql += ' %s' % self._compile_limit(query, query.limit_) return sql def _wrap_value(self, value): """ Wrap a single string in keyword identifers :param value: The value to wrap :type value: str :return: The wrapped value :rtype: str """ if value == '*': return value return '`%s`' % value.replace('`', '``') PK!x)orator/query/grammars/postgres_grammar.py# -*- coding: utf-8 -*- from .grammar import QueryGrammar from ...utils import basestring class PostgresQueryGrammar(QueryGrammar): _operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', 'like', 'not like', 'between', 'ilike', '&', '|', '#', '<<', '>>' ] marker = '%s' def _compile_lock(self, query, value): """ Compile the lock into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :param value: The lock value :type value: bool or str :return: The compiled lock :rtype: str """ if isinstance(value, basestring): return value if value: return 'FOR UPDATE' return 'FOR SHARE' def compile_update(self, query, values): """ Compile an update statement into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :param values: The update values :type values: dict :return: The compiled update :rtype: str """ table = self.wrap_table(query.from__) columns = self._compile_update_columns(values) from_ = self._compile_update_from(query) where = self._compile_update_wheres(query) return ('UPDATE %s SET %s%s %s' % (table, columns, from_, where)).strip() def _compile_update_columns(self, values): """ Compile the columns for the update statement :param values: The columns :type values: dict :return: The compiled columns :rtype: str """ columns = [] for key, value in values.items(): columns.append('%s = %s' % (self.wrap(key), self.parameter(value))) return ', '.join(columns) def _compile_update_from(self, query): """ Compile the "from" clause for an update with a join. :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled sql :rtype: str """ if not query.joins: return '' froms = [] for join in query.joins: froms.append(self.wrap_table(join.table)) if len(froms): return ' FROM %s' % ', '.join(froms) return '' def _compile_update_wheres(self, query): """ Compile the additional where clauses for updates with joins. :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled sql :rtype: str """ base_where = self._compile_wheres(query) if not query.joins: return base_where join_where = self._compile_update_join_wheres(query) if not base_where.strip(): return 'WHERE %s' % self._remove_leading_boolean(join_where) return '%s %s' % (base_where, join_where) def _compile_update_join_wheres(self, query): """ Compile the "join" clauses for an update. :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled sql :rtype: str """ join_wheres = [] for join in query.joins: for clause in join.clauses: join_wheres.append(self._compile_join_constraints(clause)) return ' '.join(join_wheres) def compile_insert_get_id(self, query, values, sequence=None): """ Compile an insert and get ID statement into SQL. :param query: A QueryBuilder instance :type query: QueryBuilder :param values: The values to insert :type values: dict :param sequence: The id sequence :type sequence: str :return: The compiled statement :rtype: str """ if sequence is None: sequence = 'id' return '%s RETURNING %s'\ % (self.compile_insert(query, values), self.wrap(sequence)) def compile_truncate(self, query): """ Compile a truncate table statement into SQL. :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled statement :rtype: str """ return { 'TRUNCATE %s RESTART IDENTITY' % self.wrap_table(query.from__): {} } PK!AXX'orator/query/grammars/sqlite_grammar.py# -*- coding: utf-8 -*- from .grammar import QueryGrammar class SQLiteQueryGrammar(QueryGrammar): _operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', 'like', 'not like', 'between', 'ilike', '&', '|', '<<', '>>', ] def compile_insert(self, query, values): """ Compile insert statement into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :param values: The insert values :type values: dict or list :return: The compiled insert :rtype: str """ table = self.wrap_table(query.from__) if not isinstance(values, list): values = [values] # If there is only one row to insert, we just use the normal grammar if len(values) == 1: return super(SQLiteQueryGrammar, self).compile_insert(query, values) names = self.columnize(values[0].keys()) columns = [] # SQLite requires us to build the multi-row insert as a listing of select with # unions joining them together. So we'll build out this list of columns and # then join them all together with select unions to complete the queries. for column in values[0].keys(): columns.append('%s AS %s' % (self.get_marker(), self.wrap(column))) columns = [', '.join(columns)] * len(values) return 'INSERT INTO %s (%s) SELECT %s'\ % (table, names, ' UNION ALL SELECT '.join(columns)) def compile_truncate(self, query): """ Compile a truncate statement into SQL :param query: A QueryBuilder instance :type query: QueryBuilder :return: The compiled truncate statement :rtype: str """ sql = { 'DELETE FROM sqlite_sequence WHERE name = %s' % self.get_marker(): [query.from__] } sql['DELETE FROM %s' % self.wrap_table(query.from__)] = [] return sql def _where_day(self, query, where): """ Compile a "where day" clause :param query: A QueryBuilder instance :type query: QueryBuilder :param where: The condition :type where: dict :return: The compiled clause :rtype: str """ return self._date_based_where('%d', query, where) def _where_month(self, query, where): """ Compile a "where month" clause :param query: A QueryBuilder instance :type query: QueryBuilder :param where: The condition :type where: dict :return: The compiled clause :rtype: str """ return self._date_based_where('%m', query, where) def _where_year(self, query, where): """ Compile a "where year" clause :param query: A QueryBuilder instance :type query: QueryBuilder :param where: The condition :type where: dict :return: The compiled clause :rtype: str """ return self._date_based_where('%Y', query, where) def _date_based_where(self, type, query, where): """ Compiled a date where based clause :param type: The date type :type type: str :param query: A QueryBuilder instance :type query: QueryBuilder :param where: The condition :type where: dict :return: The compiled clause :rtype: str """ value = str(where['value']).zfill(2) value = self.parameter(value) return 'strftime(\'%s\', %s) %s %s'\ % (type, self.wrap(where['column']), where['operator'], value) PK!QQorator/query/join_clause.py# -*- coding: utf-8 -*- from .expression import QueryExpression class JoinClause(object): def __init__(self, table, type='inner'): self.type = type self.table = table self.clauses = [] self.bindings = [] def on(self, first, operator, second, boolean='and', where=False): self.clauses.append({ 'first': first, 'operator': operator, 'second': second, 'boolean': boolean, 'where': where }) if where: self.bindings.append(second) return self def or_on(self, first, operator, second): return self.on(first, operator, second, 'or') def where(self, first, operator, second, boolean='and'): return self.on(first, operator, second, boolean, True) def or_where(self, first, operator, second): return self.where(first, operator, second, 'or') def where_null(self, column, boolean='and'): return self.on(column, 'IS', QueryExpression('NULL'), boolean, False) def or_where_null(self, column): return self.where_null(column, 'or') def where_not_null(self, column, boolean='and'): return self.on(column, 'IS', QueryExpression('NOT NULL'), boolean, False) def or_where_not_null(self, column): return self.where_not_null(column, 'or') PK!/eE#orator/query/processors/__init__.py# -*- coding: utf-8 -*- from .processor import QueryProcessor from .mysql_processor import MySQLQueryProcessor from .postgres_processor import PostgresQueryProcessor from .sqlite_processor import SQLiteQueryProcessor PK!N*orator/query/processors/mysql_processor.py# -*- coding: utf-8 -*- from .processor import QueryProcessor class MySQLQueryProcessor(QueryProcessor): def process_insert_get_id(self, query, sql, values, sequence=None): """ Process an "insert get ID" query. :param query: A QueryBuilder instance :type query: QueryBuilder :param sql: The sql query to execute :type sql: str :param values: The value bindings :type values: list :param sequence: The ids sequence :type sequence: str :return: The inserted row id :rtype: int """ if not query.get_connection().transaction_level(): with query.get_connection().transaction(): query.get_connection().insert(sql, values) cursor = query.get_connection().get_cursor() if hasattr(cursor, 'lastrowid'): id = cursor.lastrowid else: id = query.get_connection().statement('SELECT LAST_INSERT_ID()') else: query.get_connection().insert(sql, values) cursor = query.get_connection().get_cursor() if hasattr(cursor, 'lastrowid'): id = cursor.lastrowid else: id = query.get_connection().statement('SELECT LAST_INSERT_ID()') if isinstance(id, int): return id if str(id).isdigit(): return int(id) return id def process_column_listing(self, results): """ Process the results of a column listing query :param results: The query results :type results: dict :return: The processed results :return: list """ return list(map(lambda x: x['column_name'], results)) PK!v-orator/query/processors/postgres_processor.py# -*- coding: utf-8 -*- from .processor import QueryProcessor class PostgresQueryProcessor(QueryProcessor): def process_insert_get_id(self, query, sql, values, sequence=None): """ Process an "insert get ID" query. :param query: A QueryBuilder instance :type query: QueryBuilder :param sql: The sql query to execute :type sql: str :param values: The value bindings :type values: list :param sequence: The ids sequence :type sequence: str :return: The inserted row id :rtype: int """ result = query.get_connection().select_from_write_connection(sql, values) id = result[0][0] if isinstance(id, int): return id if str(id).isdigit(): return int(id) return id def process_column_listing(self, results): """ Process the results of a column listing query :param results: The query results :type results: dict :return: The processed results :return: list """ return list(map(lambda x: x['column_name'], results)) PK!M$orator/query/processors/processor.py# -*- coding: utf-8 -*- class QueryProcessor(object): def process_select(self, query, results): """ Process the results of a "select" query :param query: A QueryBuilder instance :type query: QueryBuilder :param results: The query results :type results: dict :return: The processed results :rtype: dict """ return results def process_insert_get_id(self, query, sql, values, sequence=None): """ Process an "insert get ID" query. :param query: A QueryBuilder instance :type query: QueryBuilder :param sql: The sql query to execute :type sql: str :param values: The value bindings :type values: list :param sequence: The ids sequence :type sequence: str :return: The inserted row id :rtype: int """ query.get_connection().insert(sql, values) id = query.get_connection().get_cursor().lastrowid if isinstance(id, int): return id if str(id).isdigit(): return int(id) return id def process_column_listing(self, results): """ Process the results of a column listing query :param results: The query results :type results: dict :return: The processed results :return: dict """ return results PK!j]b+orator/query/processors/sqlite_processor.py# -*- coding: utf-8 -*- from .processor import QueryProcessor class SQLiteQueryProcessor(QueryProcessor): def process_column_listing(self, results): """ Process the results of a column listing query :param results: The query results :type results: dict :return: The processed results :return: list """ return list(map(lambda x: x['name'], results)) PK!7iHorator/schema/__init__.py# -*- coding: utf-8 -*- from .builder import SchemaBuilder from .mysql_builder import MySQLSchemaBuilder from .blueprint import Blueprint from .schema import Schema PK!{lLLorator/schema/blueprint.py# -*- coding: utf-8 -*- from ..support.fluent import Fluent class Blueprint(object): def __init__(self, table): """ :param table: The table to operate on :type table: str """ self._table = table self._columns = [] self._commands = [] self.engine = None self.charset = None self.collation = None def build(self, connection, grammar): """ Execute the blueprint against the database. :param connection: The connection to use :type connection: orator.connections.Connection :param grammar: The grammar to user :type grammar: orator.query.grammars.QueryGrammar """ for statement in self.to_sql(connection, grammar): connection.statement(statement) def to_sql(self, connection, grammar): """ Get the raw SQL statements for the blueprint. :param connection: The connection to use :type connection: orator.connections.Connection :param grammar: The grammar to user :type grammar: orator.schema.grammars.SchemaGrammar :rtype: list """ self._add_implied_commands() statements = [] for command in self._commands: method = 'compile_%s' % command.name if hasattr(grammar, method): sql = getattr(grammar, method)(self, command, connection) if sql is not None: if isinstance(sql, list): statements += sql else: statements.append(sql) return statements def _add_implied_commands(self): """ Add the commands that are implied by the blueprint. """ if len(self.get_added_columns()) and not self._creating(): self._commands.insert(0, self._create_command('add')) if len(self.get_changed_columns()) and not self._creating(): self._commands.insert(0, self._create_command('change')) return self._add_fluent_indexes() def _add_fluent_indexes(self): """ Add the index commands fluently specified on columns: """ for column in self._columns: for index in ['primary', 'unique', 'index']: column_index = column.get(index) if column_index is True: getattr(self, index)(column.name) break elif column_index: getattr(self, index)(column.name, column_index) break def _creating(self): """ Determine if the blueprint has a create command. :rtype: bool """ for command in self._commands: if command.name == 'create': return True return False def create(self): """ Indicates that the table needs to be created. :rtype: Fluent """ return self._add_command('create') def drop(self): """ Indicates that the table needs to be dropped. :rtype: Fluent """ self._add_command('drop') return self def drop_if_exists(self): """ Indicates that the table should be dropped if it exists. :rtype: Fluent """ return self._add_command('drop_if_exists') def drop_column(self, *columns): """ Indicates that the given columns should be dropped. :param columns: The columns to drop :type columns: tuple :rtype: Fluent """ columns = list(columns) return self._add_command('drop_column', columns=columns) def rename_column(self, from_, to): """ Indicates that the given columns should be renamed. :param from_: The original column name :type from_: str :param to: The new name of the column :type to: str :rtype: Fluent """ return self._add_command('rename_column', **{'from_': from_, 'to': to}) def drop_primary(self, index=None): """ Indicate that the given primary key should be dropped. :param index: The index :type index: str :rtype: dict """ return self._drop_index_command('drop_primary', 'primary', index) def drop_unique(self, index): """ Indicate that the given unique key should be dropped. :param index: The index :type index: str :rtype: Fluent """ return self._drop_index_command('drop_unique', 'unique', index) def drop_index(self, index): """ Indicate that the given index should be dropped. :param index: The index :type index: str :rtype: Fluent """ return self._drop_index_command('drop_index', 'index', index) def drop_foreign(self, index): """ Indicate that the given foreign key should be dropped. :param index: The index :type index: str :rtype: dict """ return self._drop_index_command('drop_foreign', 'foreign', index) def drop_timestamps(self): """ Indicate that the timestamp columns should be dropped. :rtype: Fluent """ return self.drop_column('created_at', 'updated_at') def drop_soft_deletes(self): """ Indicate that the soft delete column should be dropped :rtype: Fluent """ return self.drop_column('deleted_at') def rename(self, to): """ Rename the table to a given name :param to: The new table name :type to: str :rtype: Fluent """ return self._add_command('rename', to=to) def primary(self, columns, name=None): """ Specify the primary key(s) for the table :param columns: The primary key(s) columns :type columns: str or list :param name: The name of the primary key :type name: str :rtype: Fluent """ return self._index_command('primary', columns, name) def unique(self, columns, name=None): """ Specify a unique index on the table :param columns: The primary key(s) columns :type columns: str or list :param name: The name of the primary key :type name: str :rtype: Fluent """ return self._index_command('unique', columns, name) def index(self, columns, name=None): """ Specify an index on the table :param columns: The primary key(s) columns :type columns: str or list :param name: The name of the primary key :type name: str :rtype: Fluent """ return self._index_command('index', columns, name) def foreign(self, columns, name=None): """ Specify an foreign key on the table :param columns: The foreign key(s) columns :type columns: str or list :param name: The name of the foreign key :type name: str :rtype: Fluent """ return self._index_command('foreign', columns, name) def increments(self, column): """ Create a new auto-incrementing integer column on the table. :param column: The auto-incrementing column :type column: str :rtype: Fluent """ return self.unsigned_integer(column, True) def big_increments(self, column): """ Create a new auto-incrementing big integer column on the table. :param column: The auto-incrementing column :type column: str :rtype: Fluent """ return self.unsigned_big_integer(column, True) def char(self, column, length=255): """ Create a new char column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('char', column, length=length) def string(self, column, length=255): """ Create a new string column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('string', column, length=length) def text(self, column): """ Create a new text column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('text', column) def medium_text(self, column): """ Create a new medium text column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('medium_text', column) def long_text(self, column): """ Create a new long text column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('long_text', column) def integer(self, column, auto_increment=False, unsigned=False): """ Create a new integer column on the table. :param column: The column :type column: str :type auto_increment: bool :type unsigned: bool :rtype: Fluent """ return self._add_column('integer', column, auto_increment=auto_increment, unsigned=unsigned) def big_integer(self, column, auto_increment=False, unsigned=False): """ Create a new big integer column on the table. :param column: The column :type column: str :type auto_increment: bool :type unsigned: bool :rtype: Fluent """ return self._add_column('big_integer', column, auto_increment=auto_increment, unsigned=unsigned) def medium_integer(self, column, auto_increment=False, unsigned=False): """ Create a new medium integer column on the table. :param column: The column :type column: str :type auto_increment: bool :type unsigned: bool :rtype: Fluent """ return self._add_column('medium_integer', column, auto_increment=auto_increment, unsigned=unsigned) def tiny_integer(self, column, auto_increment=False, unsigned=False): """ Create a new tiny integer column on the table. :param column: The column :type column: str :type auto_increment: bool :type unsigned: bool :rtype: Fluent """ return self._add_column('tiny_integer', column, auto_increment=auto_increment, unsigned=unsigned) def small_integer(self, column, auto_increment=False, unsigned=False): """ Create a new small integer column on the table. :param column: The column :type column: str :type auto_increment: bool :type unsigned: bool :rtype: Fluent """ return self._add_column('small_integer', column, auto_increment=auto_increment, unsigned=unsigned) def unsigned_integer(self, column, auto_increment=False): """ Create a new unisgned integer column on the table. :param column: The column :type column: str :type auto_increment: bool :rtype: Fluent """ return self.integer(column, auto_increment, True) def unsigned_big_integer(self, column, auto_increment=False): """ Create a new unsigned big integer column on the table. :param column: The column :type column: str :type auto_increment: bool :rtype: Fluent """ return self.big_integer(column, auto_increment, True) def float(self, column, total=8, places=2): """ Create a new float column on the table. :param column: The column :type column: str :type total: int :type places: 2 :rtype: Fluent """ return self._add_column('float', column, total=total, places=places) def double(self, column, total=None, places=None): """ Create a new double column on the table. :param column: The column :type column: str :type total: int :type places: 2 :rtype: Fluent """ return self._add_column('double', column, total=total, places=places) def decimal(self, column, total=8, places=2): """ Create a new decimal column on the table. :param column: The column :type column: str :type total: int :type places: 2 :rtype: Fluent """ return self._add_column('decimal', column, total=total, places=places) def boolean(self, column): """ Create a new decimal column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('boolean', column) def enum(self, column, allowed): """ Create a new enum column on the table. :param column: The column :type column: str :type allowed: list :rtype: Fluent """ return self._add_column('enum', column, allowed=allowed) def json(self, column): """ Create a new json column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('json', column) def date(self, column): """ Create a new date column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('date', column) def datetime(self, column): """ Create a new datetime column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('datetime', column) def time(self, column): """ Create a new time column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('time', column) def timestamp(self, column): """ Create a new timestamp column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('timestamp', column) def nullable_timestamps(self): """ Create nullable creation and update timestamps to the table. :rtype: Fluent """ self.timestamp('created_at').nullable() self.timestamp('updated_at').nullable() def timestamps(self, use_current=True): """ Create creation and update timestamps to the table. :rtype: Fluent """ if use_current: self.timestamp('created_at').use_current() self.timestamp('updated_at').use_current() else: self.timestamp('created_at') self.timestamp('updated_at') def soft_deletes(self): """ Add a "deleted at" timestamp to the table. :rtype: Fluent """ return self.timestamp('deleted_at').nullable() def binary(self, column): """ Create a new binary column on the table. :param column: The column :type column: str :rtype: Fluent """ return self._add_column('binary', column) def morphs(self, name, index_name=None): """ Add the proper columns for a polymorphic table. :type name: str :type index_name: str """ self.unsigned_integer('%s_id' % name) self.string('%s_type' % name) self.index(['%s_id' % name, '%s_type' % name], index_name) def _drop_index_command(self, command, type, index): """ Create a new drop index command on the blueprint. :param command: The command :type command: str :param type: The index type :type type: str :param index: The index name :type index: str :rtype: Fluent """ columns = [] if isinstance(index, list): columns = index index = self._create_index_name(type, columns) return self._index_command(command, columns, index) def _index_command(self, type, columns, index): """ Add a new index command to the blueprint. :param type: The index type :type type: str :param columns: The index columns :type columns: list or str :param index: The index name :type index: str :rtype: Fluent """ if not isinstance(columns, list): columns = [columns] if not index: index = self._create_index_name(type, columns) return self._add_command(type, index=index, columns=columns) def _create_index_name(self, type, columns): if not isinstance(columns, list): columns = [columns] index = '%s_%s_%s' % (self._table, '_'.join([str(column) for column in columns]), type) return index.lower().replace('-', '_').replace('.', '_') def _add_column(self, type, name, **parameters): """ Add a new column to the blueprint. :param type: The column type :type type: str :param name: The column name :type name: str :param parameters: The column parameters :type parameters: dict :rtype: Fluent """ parameters.update({ 'type': type, 'name': name }) column = Fluent(**parameters) self._columns.append(column) return column def _remove_column(self, name): """ Removes a column from the blueprint. :param name: The column name :type name: str :rtype: Blueprint """ self._columns = filter(lambda c: c.name != name, self._columns) return self def _add_command(self, name, **parameters): """ Add a new command to the blueprint. :param name: The command name :type name: str :param parameters: The command parameters :type parameters: dict :rtype: Fluent """ command = self._create_command(name, **parameters) self._commands.append(command) return command def _create_command(self, name, **parameters): """ Create a new command. :param name: The command name :type name: str :param parameters: The command parameters :type parameters: dict :rtype: Fluent """ parameters.update({'name': name}) return Fluent(**parameters) def get_table(self): return self._table def get_columns(self): return self._columns def get_commands(self): return self._commands def get_added_columns(self): return list(filter(lambda column: not column.get('change'), self._columns)) def get_changed_columns(self): return list(filter(lambda column: column.get('change'), self._columns)) PK!5""orator/schema/builder.py# -*- coding: utf-8 -*- from contextlib import contextmanager from .blueprint import Blueprint class SchemaBuilder(object): def __init__(self, connection): """ :param connection: The schema connection :type connection: orator.connections.Connection """ self._connection = connection self._grammar = connection.get_schema_grammar() def has_table(self, table): """ Determine if the given table exists. :param table: The table :type table: str :rtype: bool """ sql = self._grammar.compile_table_exists() table = self._connection.get_table_prefix() + table return len(self._connection.select(sql, [table])) > 0 def has_column(self, table, column): """ Determine if the given table has a given column. :param table: The table :type table: str :type column: str :rtype: bool """ column = column.lower() return column in list(map(lambda x: x.lower(), self.get_column_listing(table))) def get_column_listing(self, table): """ Get the column listing for a given table. :param table: The table :type table: str :rtype: list """ table = self._connection.get_table_prefix() + table results = self._connection.select(self._grammar.compile_column_exists(table)) return self._connection.get_post_processor().process_column_listing(results) @contextmanager def table(self, table): """ Modify a table on the schema. :param table: The table """ try: blueprint = self._create_blueprint(table) yield blueprint except Exception as e: raise try: self._build(blueprint) except Exception: raise @contextmanager def create(self, table): """ Create a new table on the schema. :param table: The table :type table: str :rtype: Blueprint """ try: blueprint = self._create_blueprint(table) blueprint.create() yield blueprint except Exception as e: raise try: self._build(blueprint) except Exception: raise def drop(self, table): """ Drop a table from the schema. :param table: The table :type table: str """ blueprint = self._create_blueprint(table) blueprint.drop() self._build(blueprint) def drop_if_exists(self, table): """ Drop a table from the schema. :param table: The table :type table: str """ blueprint = self._create_blueprint(table) blueprint.drop_if_exists() self._build(blueprint) def rename(self, from_, to): """ Rename a table on the schema. """ blueprint = self._create_blueprint(from_) blueprint.rename(to) self._build(blueprint) def _build(self, blueprint): """ Execute the blueprint to build / modify the table. :param blueprint: The blueprint :type blueprint: orator.schema.Blueprint """ blueprint.build(self._connection, self._grammar) def _create_blueprint(self, table): return Blueprint(table) def get_connection(self): return self._connection def set_connection(self, connection): self._connection = connection return self PK!DH"orator/schema/grammars/__init__.py# -*- coding: utf-8 -*- from .grammar import SchemaGrammar from .sqlite_grammar import SQLiteSchemaGrammar from .postgres_grammar import PostgresSchemaGrammar from .mysql_grammar import MySQLSchemaGrammar PK!0E&&!orator/schema/grammars/grammar.py# -*- coding: utf-8 -*- from ...support.grammar import Grammar from ...support.fluent import Fluent from ...query.expression import QueryExpression from ...dbal.column import Column from ...dbal.table_diff import TableDiff from ...dbal.comparator import Comparator from ..blueprint import Blueprint class SchemaGrammar(Grammar): def __init__(self, connection): super(SchemaGrammar, self).__init__(marker=connection.get_marker()) self._connection = connection def compile_rename_column(self, blueprint, command, connection): """ Compile a rename column command. :param blueprint: The blueprint :type blueprint: Blueprint :param command: The command :type command: Fluent :param connection: The connection :type connection: orator.connections.Connection :rtype: list """ schema = connection.get_schema_manager() table = self.get_table_prefix() + blueprint.get_table() column = connection.get_column(table, command.from_) table_diff = self._get_renamed_diff(blueprint, command, column, schema) return schema.get_database_platform().get_alter_table_sql(table_diff) def _get_renamed_diff(self, blueprint, command, column, schema): """ Get a new column instance with the new column name. :param blueprint: The blueprint :type blueprint: Blueprint :param command: The command :type command: Fluent :param column: The column :type column: orator.dbal.Column :param schema: The schema :type schema: orator.dbal.SchemaManager :rtype: orator.dbal.TableDiff """ table_diff = self._get_table_diff(blueprint, schema) return self._set_renamed_columns(table_diff, command, column) def _set_renamed_columns(self, table_diff, command, column): """ Set the renamed columns on the table diff. :rtype: orator.dbal.TableDiff """ new_column = Column(command.to, column.get_type(), column.to_dict()) table_diff.renamed_columns = {command.from_: new_column} return table_diff def compile_foreign(self, blueprint, command, _): """ Compile a foreign key command. :param blueprint: The blueprint :type blueprint: Blueprint :param command: The command :type command: Fluent :rtype: str """ table = self.wrap_table(blueprint) on = self.wrap_table(command.on) columns = self.columnize(command.columns) on_columns = self.columnize(command.references if isinstance(command.references, list) else [command.references]) sql = 'ALTER TABLE %s ADD CONSTRAINT %s ' % (table, command.index) sql += 'FOREIGN KEY (%s) REFERENCES %s (%s)' % (columns, on, on_columns) if command.get('on_delete'): sql += ' ON DELETE %s' % command.on_delete if command.get('on_update'): sql += ' ON UPDATE %s' % command.on_update return sql def _get_columns(self, blueprint): """ Get the blueprint's columns definitions. :param blueprint: The blueprint :type blueprint: Blueprint :rtype: list """ columns = [] for column in blueprint.get_added_columns(): sql = self.wrap(column) + ' ' + self._get_type(column) columns.append(self._add_modifiers(sql, blueprint, column)) return columns def _add_modifiers(self, sql, blueprint, column): """ Add the column modifiers to the deifinition """ for modifier in self._modifiers: method = '_modify_%s' % modifier if hasattr(self, method): sql += getattr(self, method)(blueprint, column) return sql def _get_command_by_name(self, blueprint, name): """ Get the primary key command it it exists. """ commands = self._get_commands_by_name(blueprint, name) if len(commands): return commands[0] def _get_commands_by_name(self, blueprint, name): """ Get all of the commands with a given name. """ return list(filter(lambda value: value.name == name, blueprint.get_commands())) def _get_type(self, column): """ Get the SQL for the column data type. :param column: The column :type column: Fluent :rtype sql """ return getattr(self, '_type_%s' % column.type)(column) def prefix_list(self, prefix, values): """ Add a prefix to a list of values. """ return list(map(lambda value: prefix + ' ' + value, values)) def wrap_table(self, table): if isinstance(table, Blueprint): table = table.get_table() return super(SchemaGrammar, self).wrap_table(table) def wrap(self, value, prefix_alias=False): if isinstance(value, Fluent): value = value.name return super(SchemaGrammar, self).wrap(value, prefix_alias) def _get_default_value(self, value): """ Format a value so that it can be used in "default" clauses. """ if isinstance(value, QueryExpression): return value if isinstance(value, bool): return "'%s'" % int(value) return "'%s'" % value def _get_table_diff(self, blueprint, schema): table = self.get_table_prefix() + blueprint.get_table() table_diff = TableDiff(table) table_diff.from_table = schema.list_table_details(table) return table_diff def compile_change(self, blueprint, command, connection): """ Compile a change column command into a series of SQL statement. :param blueprint: The blueprint :type blueprint: Blueprint :param command: The command :type command: Fluent :param connection: The connection :type connection: orator.connections.Connection :rtype: list """ schema = connection.get_schema_manager() table_diff = self._get_changed_diff(blueprint, schema) if table_diff: sql = schema.get_database_platform().get_alter_table_sql(table_diff) if isinstance(sql, list): return sql return [sql] return [] def _get_changed_diff(self, blueprint, schema): """ Get the table diffrence for the given changes. :param blueprint: The blueprint :type blueprint: Blueprint :param schema: The schema :type schema: orator.dbal.SchemaManager :rtype: orator.dbal.TableDiff """ table = schema.list_table_details(self.get_table_prefix() + blueprint.get_table()) return Comparator().diff_table(table, self._get_table_with_column_changes(blueprint, table)) def _get_table_with_column_changes(self, blueprint, table): """ Get a copy of the given table after making the column changes. :param blueprint: The blueprint :type blueprint: Blueprint :type table: orator.dbal.table.Table :rtype: orator.dbal.table.Table """ table = table.clone() for fluent in blueprint.get_changed_columns(): column = self._get_column_for_change(table, fluent) for key, value in fluent.get_attributes().items(): option = self._map_fluent_option(key) if option is not None: method = 'set_%s' % option if hasattr(column, method): getattr(column, method)(self._map_fluent_value(option, value)) return table def _get_column_for_change(self, table, fluent): """ Get the column instance for a column change. :type table: orator.dbal.table.Table :rtype: orator.dbal.column.Column """ return table.change_column( fluent.name, self._get_column_change_options(fluent) ).get_column(fluent.name) def _get_column_change_options(self, fluent): """ Get the column change options. """ options = { 'name': fluent.name, 'type': self._get_dbal_column_type(fluent.type), 'default': fluent.get('default') } if fluent.type in ['string']: options['length'] = fluent.length return options def _get_dbal_column_type(self, type_): """ Get the dbal column type. :param type_: The fluent type :type type_: str :rtype: str """ type_ = type_.lower() if type_ == 'big_integer': type_ = 'bigint' elif type == 'small_integer': type_ = 'smallint' elif type_ in ['medium_text', 'long_text']: type_ = 'text' return type_ def _map_fluent_option(self, attribute): if attribute in ['type', 'name']: return elif attribute == 'nullable': return 'notnull' elif attribute == 'total': return 'precision' elif attribute == 'places': return 'scale' else: return def _map_fluent_value(self, option, value): if option == 'notnull': return not value return value def platform_version(self, parts=2): return self._connection.server_version[:parts] def platform(self): """ Returns the dbal database platform. :rtype: orator.dbal.platforms.platform.Platform """ return self._connection.get_database_platform() PK!ꩩ 'orator/schema/grammars/mysql_grammar.py# -*- coding: utf-8 -*- from .grammar import SchemaGrammar from ..blueprint import Blueprint from ...query.expression import QueryExpression from ...support.fluent import Fluent class MySQLSchemaGrammar(SchemaGrammar): _modifiers = [ 'unsigned', 'charset', 'collate', 'nullable', 'default', 'increment', 'comment', 'after' ] _serials = ['big_integer', 'integer', 'medium_integer', 'small_integer', 'tiny_integer'] marker = '%s' def compile_table_exists(self): """ Compile the query to determine if a table exists :rtype: str """ return 'SELECT * ' \ 'FROM information_schema.tables ' \ 'WHERE table_schema = %(marker)s ' \ 'AND table_name = %(marker)s' % {'marker': self.get_marker()} def compile_column_exists(self): """ Compile the query to determine the list of columns. """ return 'SELECT column_name ' \ 'FROM information_schema.columns ' \ 'WHERE table_schema = %(marker)s AND table_name = %(marker)s' \ % {'marker': self.get_marker()} def compile_create(self, blueprint, command, connection): """ Compile a create table command. """ columns = ', '.join(self._get_columns(blueprint)) sql = 'CREATE TABLE %s (%s)' % (self.wrap_table(blueprint), columns) sql = self._compile_create_encoding(sql, connection, blueprint) if blueprint.engine: sql += ' ENGINE = %s' % blueprint.engine return sql def _compile_create_encoding(self, sql, connection, blueprint): """ Append the character set specifications to a command. :type sql: str :type connection: orator.connections.Connection :type blueprint: Blueprint :rtype: str """ charset = blueprint.charset or connection.get_config('charset') if charset: sql += ' DEFAULT CHARACTER SET %s' % charset collation = blueprint.collation or connection.get_config('collation') if collation: sql += ' COLLATE %s' % collation return sql def compile_add(self, blueprint, command, _): table = self.wrap_table(blueprint) columns = self.prefix_list('ADD', self._get_columns(blueprint)) return 'ALTER TABLE %s %s' % (table, ', '.join(columns)) def compile_primary(self, blueprint, command, _): command.name = None return self._compile_key(blueprint, command, 'PRIMARY KEY') def compile_unique(self, blueprint, command, _): return self._compile_key(blueprint, command, 'UNIQUE') def compile_index(self, blueprint, command, _): return self._compile_key(blueprint, command, 'INDEX') def _compile_key(self, blueprint, command, type): columns = self.columnize(command.columns) table = self.wrap_table(blueprint) return 'ALTER TABLE %s ADD %s %s(%s)' % (table, type, command.index, columns) def compile_drop(self, blueprint, command, _): return 'DROP TABLE %s' % self.wrap_table(blueprint) def compile_drop_if_exists(self, blueprint, command, _): return 'DROP TABLE IF EXISTS %s' % self.wrap_table(blueprint) def compile_drop_column(self, blueprint, command, connection): columns = self.prefix_list('DROP', self.wrap_list(command.columns)) table = self.wrap_table(blueprint) return 'ALTER TABLE %s %s' % (table, ', '.join(columns)) def compile_drop_primary(self, blueprint, command, _): return 'ALTER TABLE %s DROP PRIMARY KEY'\ % self.wrap_table(blueprint) def compile_drop_unique(self, blueprint, command, _): table = self.wrap_table(blueprint) return 'ALTER TABLE %s DROP INDEX %s' % (table, command.index) def compile_drop_index(self, blueprint, command, _): table = self.wrap_table(blueprint) return 'ALTER TABLE %s DROP INDEX %s' % (table, command.index) def compile_drop_foreign(self, blueprint, command, _): table = self.wrap_table(blueprint) return 'ALTER TABLE %s DROP FOREIGN KEY %s' % (table, command.index) def compile_rename(self, blueprint, command, _): from_ = self.wrap_table(blueprint) return 'RENAME TABLE %s TO %s' % (from_, self.wrap_table(command.to)) def _type_char(self, column): return "CHAR(%s)" % column.length def _type_string(self, column): return "VARCHAR(%s)" % column.length def _type_text(self, column): return 'TEXT' def _type_medium_text(self, column): return 'MEDIUMTEXT' def _type_long_text(self, column): return 'LONGTEXT' def _type_integer(self, column): return 'INT' def _type_big_integer(self, column): return 'BIGINT' def _type_medium_integer(self, column): return 'MEDIUMINT' def _type_tiny_integer(self, column): return 'TINYINT' def _type_small_integer(self, column): return 'SMALLINT' def _type_float(self, column): return self._type_double(column) def _type_double(self, column): if column.total and column.places: return 'DOUBLE(%s, %s)' % (column.total, column.places) return 'DOUBLE' def _type_decimal(self, column): return 'DECIMAL(%s, %s)' % (column.total, column.places) def _type_boolean(self, column): return 'TINYINT(1)' def _type_enum(self, column): return 'ENUM(\'%s\')' % '\', \''.join(column.allowed) def _type_json(self, column): if self.platform().has_native_json_type(): return 'JSON' return 'TEXT' def _type_date(self, column): return 'DATE' def _type_datetime(self, column): return 'DATETIME' def _type_time(self, column): return 'TIME' def _type_timestamp(self, column): platform_version = self.platform_version(3) column_type = 'TIMESTAMP' if platform_version >= (5, 6, 0): if platform_version >= (5, 6, 4): # Versions 5.6.4+ support fractional seconds column_type = 'TIMESTAMP(6)' current = 'CURRENT_TIMESTAMP(6)' else: current = 'CURRENT_TIMESTAMP' else: current = '0' if column.use_current: return '{} DEFAULT {}'.format(column_type, current) return column_type def _type_binary(self, column): return 'BLOB' def _modify_unsigned(self, blueprint, column): if column.get('unsigned', False): return ' UNSIGNED' return '' def _modify_charset(self, blueprint, column): if column.get('charset'): return ' CHARACTER SET ' + column.charset return '' def _modify_collate(self, blueprint, column): if column.get('collation'): return ' COLLATE ' + column.collation return '' def _modify_nullable(self, blueprint, column): if column.get('nullable'): return ' NULL' return ' NOT NULL' def _modify_default(self, blueprint, column): if column.get('default') is not None: return ' DEFAULT %s' % self._get_default_value(column.default) return '' def _modify_increment(self, blueprint, column): if column.type in self._serials and column.auto_increment: return ' AUTO_INCREMENT PRIMARY KEY' return '' def _modify_after(self, blueprint, column): if column.get('after') is not None: return ' AFTER ' + self.wrap(column.after) return '' def _modify_comment(self, blueprint, column): if column.get('comment') is not None: return ' COMMENT "%s"' % column.comment return '' def _get_column_change_options(self, fluent): """ Get the column change options. """ options = super(MySQLSchemaGrammar, self)._get_column_change_options(fluent) if fluent.type == 'enum': options['extra'] = { 'definition': '(\'{}\')'.format('\',\''.join(fluent.allowed)) } return options def _wrap_value(self, value): if value == '*': return value return '`%s`' % value.replace('`', '``') PK!l333*orator/schema/grammars/postgres_grammar.py# -*- coding: utf-8 -*- from .grammar import SchemaGrammar from ..blueprint import Blueprint from ...query.expression import QueryExpression from ...support.fluent import Fluent class PostgresSchemaGrammar(SchemaGrammar): _modifiers = ['increment', 'nullable', 'default'] _serials = ['big_integer', 'integer', 'medium_integer', 'small_integer', 'tiny_integer'] marker = '%s' def compile_rename_column(self, blueprint, command, connection): """ Compile a rename column command. :param blueprint: The blueprint :type blueprint: Blueprint :param command: The command :type command: Fluent :param connection: The connection :type connection: orator.connections.Connection :rtype: list """ table = self.get_table_prefix() + blueprint.get_table() column = self.wrap(command.from_) return 'ALTER TABLE %s RENAME COLUMN %s TO %s'\ % (table, column, self.wrap(command.to)) def compile_table_exists(self): """ Compile the query to determine if a table exists :rtype: str """ return 'SELECT * ' \ 'FROM information_schema.tables ' \ 'WHERE table_name = %(marker)s' \ % {'marker': self.get_marker()} def compile_column_exists(self, table): """ Compile the query to determine the list of columns. """ return 'SELECT column_name ' \ 'FROM information_schema.columns ' \ 'WHERE table_name = \'%s\'' % table def compile_create(self, blueprint, command, _): """ Compile a create table command. """ columns = ', '.join(self._get_columns(blueprint)) return 'CREATE TABLE %s (%s)' % (self.wrap_table(blueprint), columns) def compile_add(self, blueprint, command, _): table = self.wrap_table(blueprint) columns = self.prefix_list('ADD COLUMN', self._get_columns(blueprint)) return 'ALTER TABLE %s %s' % (table, ', '.join(columns)) def compile_primary(self, blueprint, command, _): columns = self.columnize(command.columns) return 'ALTER TABLE %s ADD PRIMARY KEY (%s)'\ % (self.wrap_table(blueprint), columns) def compile_unique(self, blueprint, command, _): columns = self.columnize(command.columns) table = self.wrap_table(blueprint) return 'ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)'\ % (table, command.index, columns) def compile_index(self, blueprint, command, _): columns = self.columnize(command.columns) table = self.wrap_table(blueprint) return 'CREATE INDEX %s ON %s (%s)' % (command.index, table, columns) def compile_drop(self, blueprint, command, _): return 'DROP TABLE %s' % self.wrap_table(blueprint) def compile_drop_if_exists(self, blueprint, command, _): return 'DROP TABLE IF EXISTS %s' % self.wrap_table(blueprint) def compile_drop_column(self, blueprint, command, connection): columns = self.prefix_list('DROP COLUMN', self.wrap_list(command.columns)) table = self.wrap_table(blueprint) return 'ALTER TABLE %s %s' % (table, ', '.join(columns)) def compile_drop_primary(self, blueprint, command, _): table = blueprint.get_table() return 'ALTER TABLE %s DROP CONSTRAINT %s_pkey'\ % (self.wrap_table(blueprint), table) def compile_drop_unique(self, blueprint, command, _): table = self.wrap_table(blueprint) return 'ALTER TABLE %s DROP CONSTRAINT %s' % (table, command.index) def compile_drop_index(self, blueprint, command, _): return 'DROP INDEX %s' % command.index def compile_drop_foreign(self, blueprint, command, _): table = self.wrap_table(blueprint) return 'ALTER TABLE %s DROP CONSTRAINT %s' % (table, command.index) def compile_rename(self, blueprint, command, _): from_ = self.wrap_table(blueprint) return 'ALTER TABLE %s RENAME TO %s' % (from_, self.wrap_table(command.to)) def _type_char(self, column): return "CHAR(%s)" % column.length def _type_string(self, column): return "VARCHAR(%s)" % column.length def _type_text(self, column): return 'TEXT' def _type_medium_text(self, column): return 'TEXT' def _type_long_text(self, column): return 'TEXT' def _type_integer(self, column): return 'SERIAL' if column.auto_increment else 'INTEGER' def _type_big_integer(self, column): return 'BIGSERIAL' if column.auto_increment else 'BIGINT' def _type_medium_integer(self, column): return 'SERIAL' if column.auto_increment else 'INTEGER' def _type_tiny_integer(self, column): return 'SMALLSERIAL' if column.auto_increment else 'SMALLINT' def _type_small_integer(self, column): return 'SMALLSERIAL' if column.auto_increment else 'SMALLINT' def _type_float(self, column): return self._type_double(column) def _type_double(self, column): return 'DOUBLE PRECISION' def _type_decimal(self, column): return 'DECIMAL(%s, %s)' % (column.total, column.places) def _type_boolean(self, column): return 'BOOLEAN' def _type_enum(self, column): allowed = list(map(lambda a: "'%s'" % a, column.allowed)) return 'VARCHAR(255) CHECK ("%s" IN (%s))' % (column.name, ', '.join(allowed)) def _type_json(self, column): return 'JSON' def _type_date(self, column): return 'DATE' def _type_datetime(self, column): return 'TIMESTAMP(6) WITHOUT TIME ZONE' def _type_time(self, column): return 'TIME(6) WITHOUT TIME ZONE' def _type_timestamp(self, column): if column.use_current: return 'TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP(6)' return 'TIMESTAMP(6) WITHOUT TIME ZONE' def _type_binary(self, column): return 'BYTEA' def _modify_nullable(self, blueprint, column): if column.get('nullable'): return ' NULL' return ' NOT NULL' def _modify_default(self, blueprint, column): if column.get('default') is not None: return ' DEFAULT %s' % self._get_default_value(column.default) return '' def _modify_increment(self, blueprint, column): if column.type in self._serials and column.auto_increment: return ' PRIMARY KEY' return '' def _get_dbal_column_type(self, type_): """ Get the dbal column type. :param type_: The fluent type :type type_: str :rtype: str """ type_ = type_.lower() if type_ == 'enum': return 'string' return super(PostgresSchemaGrammar, self)._get_dbal_column_type(type_) PK!1 1 (orator/schema/grammars/sqlite_grammar.py# -*- coding: utf-8 -*- from .grammar import SchemaGrammar from ..blueprint import Blueprint from ...query.expression import QueryExpression from ...support.fluent import Fluent class SQLiteSchemaGrammar(SchemaGrammar): _modifiers = ['nullable', 'default', 'increment'] _serials = ['big_integer', 'integer'] def compile_rename_column(self, blueprint, command, connection): """ Compile a rename column command. :param blueprint: The blueprint :type blueprint: Blueprint :param command: The command :type command: Fluent :param connection: The connection :type connection: orator.connections.Connection :rtype: list """ sql = [] # If foreign keys are on, we disable them foreign_keys = self._connection.select('PRAGMA foreign_keys') if foreign_keys: foreign_keys = bool(foreign_keys[0]) if foreign_keys: sql.append('PRAGMA foreign_keys = OFF') sql += super(SQLiteSchemaGrammar, self).compile_rename_column(blueprint, command, connection) if foreign_keys: sql.append('PRAGMA foreign_keys = ON') return sql def compile_change(self, blueprint, command, connection): """ Compile a change column command into a series of SQL statement. :param blueprint: The blueprint :type blueprint: orator.schema.Blueprint :param command: The command :type command: Fluent :param connection: The connection :type connection: orator.connections.Connection :rtype: list """ sql = [] # If foreign keys are on, we disable them foreign_keys = self._connection.select('PRAGMA foreign_keys') if foreign_keys: foreign_keys = bool(foreign_keys[0]) if foreign_keys: sql.append('PRAGMA foreign_keys = OFF') sql += super(SQLiteSchemaGrammar, self).compile_change(blueprint, command, connection) if foreign_keys: sql.append('PRAGMA foreign_keys = ON') return sql def compile_table_exists(self): """ Compile the query to determine if a table exists :rtype: str """ return "SELECT * FROM sqlite_master WHERE type = 'table' AND name = %(marker)s" % {'marker': self.get_marker()} def compile_column_exists(self, table): """ Compile the query to determine the list of columns. """ return 'PRAGMA table_info(%s)' % table.replace('.', '__') def compile_create(self, blueprint, command, _): """ Compile a create table command. """ columns = ', '.join(self._get_columns(blueprint)) sql = 'CREATE TABLE %s (%s' % (self.wrap_table(blueprint), columns) sql += self._add_foreign_keys(blueprint) sql += self._add_primary_keys(blueprint) return sql + ')' def _add_foreign_keys(self, blueprint): sql = '' foreigns = self._get_commands_by_name(blueprint, 'foreign') for foreign in foreigns: sql += self._get_foreign_key(foreign) if foreign.get('on_delete'): sql += ' ON DELETE %s' % foreign.on_delete if foreign.get('on_update'): sql += ' ON UPDATE %s' % foreign.on_delete return sql def _get_foreign_key(self, foreign): on = self.wrap_table(foreign.on) columns = self.columnize(foreign.columns) references = foreign.references if not isinstance(references, list): references = [references] on_columns = self.columnize(references) return ', FOREIGN KEY(%s) REFERENCES %s(%s)' % (columns, on, on_columns) def _add_primary_keys(self, blueprint): primary = self._get_command_by_name(blueprint, 'primary') if primary: columns = self.columnize(primary.columns) return ', PRIMARY KEY (%s)' % columns return '' def compile_add(self, blueprint, command, _): table = self.wrap_table(blueprint) columns = self.prefix_list('ADD COLUMN', self._get_columns(blueprint)) statements = [] for column in columns: statements.append('ALTER TABLE %s %s' % (table, column)) return statements def compile_unique(self, blueprint, command, _): columns = self.columnize(command.columns) table = self.wrap_table(blueprint) return 'CREATE UNIQUE INDEX %s ON %s (%s)' % (command.index, table, columns) def compile_index(self, blueprint, command, _): columns = self.columnize(command.columns) table = self.wrap_table(blueprint) return 'CREATE INDEX %s ON %s (%s)' % (command.index, table, columns) def compile_foreign(self, blueprint, command, _): pass def compile_drop(self, blueprint, command, _): return 'DROP TABLE %s' % self.wrap_table(blueprint) def compile_drop_if_exists(self, blueprint, command, _): return 'DROP TABLE IF EXISTS %s' % self.wrap_table(blueprint) def compile_drop_column(self, blueprint, command, connection): schema = connection.get_schema_manager() table_diff = self._get_table_diff(blueprint, schema) for name in command.columns: column = connection.get_column(blueprint.get_table(), name) table_diff.removed_columns[name] = column return schema.get_database_platform().get_alter_table_sql(table_diff) def compile_drop_unique(self, blueprint, command, _): return 'DROP INDEX %s' % command.index def compile_drop_index(self, blueprint, command, _): return 'DROP INDEX %s' % command.index def compile_rename(self, blueprint, command, _): from_ = self.wrap_table(blueprint) return 'ALTER TABLE %s RENAME TO %s' % (from_, self.wrap_table(command.to)) def _type_char(self, column): return 'VARCHAR' def _type_string(self, column): return 'VARCHAR' def _type_text(self, column): return 'TEXT' def _type_medium_text(self, column): return 'TEXT' def _type_long_text(self, column): return 'TEXT' def _type_integer(self, column): return 'INTEGER' def _type_big_integer(self, column): return 'INTEGER' def _type_medium_integer(self, column): return 'INTEGER' def _type_tiny_integer(self, column): return 'TINYINT' def _type_small_integer(self, column): return 'INTEGER' def _type_float(self, column): return 'FLOAT' def _type_double(self, column): return 'FLOAT' def _type_decimal(self, column): return 'NUMERIC' def _type_boolean(self, column): return 'TINYINT' def _type_enum(self, column): return 'VARCHAR' def _type_json(self, column): return 'TEXT' def _type_date(self, column): return 'DATE' def _type_datetime(self, column): return 'DATETIME' def _type_time(self, column): return 'TIME' def _type_timestamp(self, column): if column.use_current: return 'DATETIME DEFAULT CURRENT_TIMESTAMP' return 'DATETIME' def _type_binary(self, column): return 'BLOB' def _modify_nullable(self, blueprint, column): if column.get('nullable'): return ' NULL' return ' NOT NULL' def _modify_default(self, blueprint, column): if column.get('default') is not None: return ' DEFAULT %s' % self._get_default_value(column.default) return '' def _modify_increment(self, blueprint, column): if column.type in self._serials and column.auto_increment: return ' PRIMARY KEY AUTOINCREMENT' return '' def _get_dbal_column_type(self, type_): """ Get the dbal column type. :param type_: The fluent type :type type_: str :rtype: str """ type_ = type_.lower() if type_ == 'enum': return 'string' return super(SQLiteSchemaGrammar, self)._get_dbal_column_type(type_) PK!o&ETorator/schema/mysql_builder.py# -*- coding: utf-8 -*- from .builder import SchemaBuilder class MySQLSchemaBuilder(SchemaBuilder): def has_table(self, table): """ Determine if the given table exists. :param table: The table :type table: str :rtype: bool """ sql = self._grammar.compile_table_exists() database = self._connection.get_database_name() table = self._connection.get_table_prefix() + table return len(self._connection.select(sql, [database, table])) > 0 def get_column_listing(self, table): """ Get the column listing for a given table. :param table: The table :type table: str :rtype: list """ sql = self._grammar.compile_column_exists() database = self._connection.get_database_name() table = self._connection.get_table_prefix() + table results = [] for result in self._connection.select(sql, [database, table]): new_result = {} for key, value in result.items(): new_result[key.lower()] = value results.append(new_result) return self._connection.get_post_processor().process_column_listing(results) PK!coorator/schema/schema.py# -*- coding: utf-8 -*- class Schema(object): def __init__(self, manager): """ :param manager: The database manager :type manager: orator.DatabaseManager """ self.db = manager def connection(self, connection=None): """ Get a schema builder instance for a connection. :param connection: The connection to user :type connection: str :rtype: orator.schema.SchemaBuilder """ return self.db.connection(connection).get_schema_builder() def __getattr__(self, item): return getattr(self.db.connection().get_schema_builder(), item) PK!ݯ44orator/seeds/__init__.py# -*- coding: utf-8 -*- from .seeder import Seeder PK!^orator/seeds/seeder.py# -*- coding: utf-8 -*- from ..orm import Factory class Seeder(object): factory = None def __init__(self, resolver=None): self._command = None self._resolver = resolver if self.factory is None: self.factory = Factory(resolver=resolver) else: self.factory.set_connection_resolver(self._resolver) def run(self): """ Run the database seeds. """ pass def call(self, klass): """ Seed the given connection from the given class. :param klass: The Seeder class :type klass: class """ self._resolve(klass).run() if self._command: self._command.line('Seeded: %s' % klass.__name__) def _resolve(self, klass): """ Resolve an instance of the given seeder klass. :param klass: The Seeder class :type klass: class """ resolver = None if self._resolver: resolver = self._resolver elif self._command: resolver = self._command.resolver instance = klass() instance.set_connection_resolver(resolver) if self._command: instance.set_command(self._command) return instance def set_command(self, command): """ Set the console command instance. :param command: The command :type command: cleo.Command """ self._command = command return self def set_connection_resolver(self, resolver): self._resolver = resolver self.factory.set_connection_resolver(resolver) @property def db(self): return self._resolver PK!Zorator/seeds/stubs.py# -*- coding: utf-8 -*- DEFAULT_STUB = """from orator.seeds import Seeder class DummyClass(Seeder): def run(self): \"\"\" Run the database seeds. \"\"\" pass """ PK!H<<orator/support/__init__.py# -*- coding: utf-8 -*- from .collection import Collection PK!^NUyyorator/support/collection.py# -*- coding: utf-8 -*- from backpack import Collection as BaseCollection class Collection(BaseCollection): pass PK!akcorator/support/fluent.py# -*- coding: utf-8 -*- import simplejson as json from wrapt import ObjectProxy from ..utils import value class Dynamic(ObjectProxy): _key = None _fluent = None def __init__(self, value, key, fluent): super(Dynamic, self).__init__(value) self._key = key self._fluent = fluent def __call__(self, *args, **kwargs): if len(args): self.__set_value(args[0]) else: self.__set_value(True) return self._fluent def __set_value(self, value): self._fluent._attributes[self._key] = value class Fluent(object): def __init__(self, **attributes): self._attributes = {} for key, value in attributes.items(): self._attributes[key] = value def get(self, key, default=None): return self._attributes.get(key, value(default)) def get_attributes(self): return self._attributes def to_dict(self): return self.serialize() def serialize(self): return self._attributes def to_json(self, **options): return json.dumps(self.serialize(), **options) def __contains__(self, item): return item in self._attributes def __getitem__(self, item): return self._attributes[item] def __setitem__(self, key, value): self._attributes[key] = value def __delitem__(self, key): del self._attributes[key] def __dynamic(self, method): def call(*args, **kwargs): if len(args): self._attributes[method] = args[0] else: self._attributes[method] = True return self return call def __getattr__(self, item): return Dynamic(self._attributes.get(item), item, self) def __setattr__(self, key, value): if key == '_attributes': super(Fluent, self).__setattr__(key, value) try: super(Fluent, self).__getattribute__(key) return super(Fluent, self).__setattr__(key, value) except AttributeError: pass self._attributes[key] = value def __delattr__(self, item): del self._attributes[item] PK!TW W orator/support/grammar.py# -*- coding: utf-8 -*- from ..query.expression import QueryExpression class Grammar(object): marker = '?' def __init__(self, marker=None): self._table_prefix = '' if marker: self.marker = marker def wrap_list(self, values): return list(map(self.wrap, values)) def wrap_table(self, table): if self.is_expression(table): return self.get_value(table) return self.wrap(self._table_prefix + str(table), True) def wrap(self, value, prefix_alias=False): if self.is_expression(value): return self.get_value(value) # If the value being wrapped has a column alias we will need # to separate out the pieces so we can wrap each of the segments # of the expression on it own, and then joins them # both back together with the "as" connector. if value.lower().find(' as ') >= 0: segments = value.split(' ') if prefix_alias: segments[2] = self._table_prefix + segments[2] return '%s AS %s' % (self.wrap(segments[0]), self._wrap_value(segments[2])) wrapped = [] segments = value.split('.') # If the value is not an aliased table expression, we'll just wrap it like # normal, so if there is more than one segment, we will wrap the first # segments as if it was a table and the rest as just regular values. for key, segment in enumerate(segments): if key == 0 and len(segments) > 1: wrapped.append(self.wrap_table(segment)) else: wrapped.append(self._wrap_value(segment)) return '.'.join(wrapped) def _wrap_value(self, value): if value == '*': return value return '"%s"' % value.replace('"', '""') def columnize(self, columns): return ', '.join(map(self.wrap, columns)) def parameterize(self, values): return ', '.join(map(self.parameter, values)) def parameter(self, value): if self.is_expression(value): return self.get_value(value) return self.get_marker() def get_value(self, expression): return expression.get_value() def is_expression(self, value): return isinstance(value, QueryExpression) def get_date_format(self): return '%Y-%m-%d %H:%M:%S.%f' def get_table_prefix(self): return self._table_prefix def set_table_prefix(self, prefix): self._table_prefix = prefix return self def get_marker(self): return self.marker PK!;L L orator/utils/__init__.py# -*- coding: utf-8 -*- import sys import warnings import functools PY2 = sys.version_info[0] == 2 PY3K = sys.version_info[0] >= 3 PY33 = sys.version_info >= (3, 3) if PY2: import imp long = long unicode = unicode basestring = basestring reduce = reduce from urllib import quote_plus, unquote_plus, quote, unquote from urlparse import parse_qsl def load_module(module, path): with open(path, 'rb') as fh: mod = imp.load_source(module, path, fh) return mod else: long = int unicode = str basestring = str from functools import reduce from urllib.parse import (quote_plus, unquote_plus, parse_qsl, quote, unquote) if PY33: from importlib import machinery def load_module(module, path): return machinery.SourceFileLoader( module, path ).load_module(module) else: import imp def load_module(module, path): with open(path, 'rb') as fh: mod = imp.load_source(module, path, fh) return mod from .helpers import mkdir_p, value class Null(object): def __bool__(self): return False def __eq__(self, other): return other is None def deprecated(func): '''This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used.''' @functools.wraps(func) def new_func(*args, **kwargs): if PY3K: func_code = func.__code__ else: func_code = func.func_code warnings.warn_explicit( "Call to deprecated function {}.".format(func.__name__), category=DeprecationWarning, filename=func_code.co_filename, lineno=func_code.co_firstlineno + 1 ) return func(*args, **kwargs) return new_func def decode(string, encodings=None): if not PY2 and not isinstance(string, bytes): return string if PY2 and isinstance(string, unicode): return string if encodings is None: encodings = ['utf-8', 'latin1', 'ascii'] for encoding in encodings: try: return string.decode(encoding) except (UnicodeEncodeError, UnicodeDecodeError): pass return string.decode(encodings[0], errors='ignore') def encode(string, encodings=None): if not PY2 and isinstance(string, bytes): return string if PY2 and isinstance(string, str): return string if encodings is None: encodings = ['utf-8', 'latin1', 'ascii'] for encoding in encodings: try: return string.encode(encoding) except (UnicodeEncodeError, UnicodeDecodeError): pass return string.encode(encodings[0], errors='ignore') PK!7:??!orator/utils/command_formatter.py# -*- coding: utf-8 -*- from pygments.formatter import Formatter from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic, Token, Whitespace from pygments.util import get_choice_opt COMMAND_COLORS = { Token: ('', ''), Whitespace: ('fg=white', 'fg=black;options=bold'), Comment: ('fg=white', 'fg=black;options=bold'), Comment.Preproc: ('fg=cyan', 'fg=cyan;options=bold'), Keyword: ('fg=blue', 'fg=blue;options=bold'), Keyword.Type: ('fg=cyan', 'fg=cyan;options=bold'), Operator.Word: ('fg=magenta', 'fg=magenta;options=bold'), Name.Builtin: ('fg=cyan', 'fg=cyan;options=bold'), Name.Function: ('fg=green', 'fg=green;option=bold'), Name.Namespace: ('fg=cyan;options=underline', 'fg=cyan;options=bold,underline'), Name.Class: ('fg=green;options=underline', 'fg=green;options=bold,underline'), Name.Exception: ('fg=cyan', 'fg=cyan;options=bold'), Name.Decorator: ('fg=black;options=bold', 'fg=white'), Name.Variable: ('fg=red', 'fg=red;options=bold'), Name.Constant: ('fg=red', 'fg=red;options=bold'), Name.Attribute: ('fg=cyan', 'fg=cyan;options=bold'), Name.Tag: ('fg=blue;options=bold', 'fg=blue;options=bold'), String: ('fg=yellow', 'fg=yellow'), Number: ('fg=blue', 'fg=blue;options=bold'), Generic.Deleted: ('fg=red;options=bold', 'fg=red;options=bold'), Generic.Inserted: ('fg=green', 'fg=green;options=bold'), Generic.Heading: ('options=bold', 'option=bold'), Generic.Subheading: ('fg=magenta;options=bold', 'fg=magenta;options=bold'), Generic.Prompt: ('options=bold', 'options=bold'), Generic.Error: ('fg=red;options=bold', 'fg=red;options=bold'), Error: ('fg=red;options=bold,underline', 'fg=red;options=bold,underline'), } class CommandFormatter(Formatter): r""" Format tokens with Cleo color sequences, for output in a text console. Color sequences are terminated at newlines, so that paging the output works correctly. The `get_style_defs()` method doesn't do anything special since there is no support for common styles. Options accepted: `bg` Set to ``"light"`` or ``"dark"`` depending on the terminal's background (default: ``"light"``). `colorscheme` A dictionary mapping token types to (lightbg, darkbg) color names or ``None`` (default: ``None`` = use builtin colorscheme). `linenos` Set to ``True`` to have line numbers on the terminal output as well (default: ``False`` = no line numbers). """ name = 'Command' aliases = ['command'] filenames = [] def __init__(self, **options): Formatter.__init__(self, **options) self.darkbg = get_choice_opt(options, 'bg', ['light', 'dark'], 'light') == 'dark' self.colorscheme = options.get('colorscheme', None) or COMMAND_COLORS self.linenos = options.get('linenos', False) self._lineno = 0 def format(self, tokensource, outfile): return Formatter.format(self, tokensource, outfile) def _write_lineno(self, outfile): self._lineno += 1 outfile.write("%s%04d: " % (self._lineno != 1 and '\n' or '', self._lineno)) def _get_color(self, ttype): # self.colorscheme is a dict containing usually generic types, so we # have to walk the tree of dots. The base Token type must be a key, # even if it's empty string, as in the default above. colors = self.colorscheme.get(ttype) while colors is None: ttype = ttype.parent colors = self.colorscheme.get(ttype) return colors[self.darkbg] def format_unencoded(self, tokensource, outfile): if self.linenos: self._write_lineno(outfile) for ttype, value in tokensource: color = self._get_color(ttype) for line in value.splitlines(True): if color: outfile.write('<%s>%s' % (color, line.rstrip('\n'))) else: outfile.write(line.rstrip('\n')) if line.endswith('\n'): if self.linenos: self._write_lineno(outfile) else: outfile.write('\n') if self.linenos: outfile.write("\n") PK!ֈorator/utils/helpers.py# -*- coding: utf-8 -*- import os import errno import datetime def value(val): if callable(val): return val() return val def mkdir_p(path, mode=0o777): try: os.makedirs(path, mode) except OSError as exc: if exc.errno == errno.EEXIST and os.path.isdir(path): pass else: raise def serialize(value): if isinstance(value, datetime.datetime): if hasattr(value, 'to_json'): value = value.to_json() else: value = value.isoformat() elif isinstance(value, list): value = list(map(serialize, value)) elif isinstance(value, dict): for k, v in value.items(): value[k] = serialize(v) return value PK!"[orator/utils/qmarker.py# -*- coding: utf-8 -*- import re class Qmarker(object): RE_QMARK = re.compile(r'\?\?|\?|%') @classmethod def qmark(cls, query): """ Convert a "qmark" query into "format" style. """ def sub_sequence(m): s = m.group(0) if s == '??': return '?' if s == '%': return '%%' else: return '%s' return cls.RE_QMARK.sub(sub_sequence, query) @classmethod def denullify(cls, args): for arg in args: if arg is not None: yield arg else: yield () qmark = Qmarker.qmark denullify = Qmarker.denullify PK!( wssorator/utils/url.py# -*- coding: utf-8 -*- # 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 parse_qsl, unquote_plus, unquote, basestring, PY2 from ..exceptions import ArgumentError class URL(object): """ Represent the components of a URL used to connect to a database. All initialization parameters are available as public attributes. :param drivername: the name of the database backend. :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_backend_name(self): if '+' not in self.drivername: return self.drivername else: return self.drivername.split('+')[0] def get_driver_name(self): if '+' not in self.drivername: return self.get_dialect().driver else: return self.drivername.split('+')[1] 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, basestring): 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(parse_qsl(tokens[1]))) or None if PY2 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 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 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(parse_qsl(args)) return URL(name, *opts) else: return None PK!H ~#:F'orator-0.9.8.dist-info/entry_points.txtN+I/N.,()/J,/Pzy)z9ɉ%yVHl<..PK!P&&orator-0.9.8.dist-info/LICENSECopyright (c) 2015 Sébastien Eustace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H,kXZorator-0.9.8.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]OYE͕WQmfK_> wPK!Hn: Vorator-0.9.8.dist-info/METADATA U!(Y+P)YѸiPFZi 0SI)K{?3u :-ˬ8~W?H'(PI./nBawZ4?7T"ΫҏLF(~W(*Uqnc'GÑ|^Dz(q 8>W74C qcʻ*Oɓ"L*vϒ'81*C zX|mazޗtE)Ȁc-D }K[ Xl(w{P AU%3/, ™L+X(@3aT3DJރא# hrV]L+<#1MN?%D $E88}J`#LBƷ=&rsl, e$AQwhX{fFDVybs:GA$c,-"v$*Ո(_3&),h@h@H=E&$TT,i3{=Cg M0T1u$D0;ߥE9b84&e8dWmC _Fn>Q8 \U * c9Ƹ/Pe{`;JhꏣIw T# JJ߁yXa!`l^kf)< =1H;C/e@">8iUĴ%!#1JTϲw V_\䘇3DA_CL;?a#~N2/K 8~^ZD+i2!'>_yqz//K2=@< U1 GP(Q$QK& $jc+y?1`PuCR‘}O`*J2b`ڋLd}0w\ނ0h&BzDp^n[egdY. >p˅$}w4k8(ŀPhl)Zao[8詳<|NܩXB (k2ҳ0I$9yqކ4J>mtnˢ #k`*WULh1M+nPͧ ֞*iFPD7+dkkG6LgKmZ툱Y i'JX$p ᐠxO +6S0%N $ >8,`"18DG u񱏂RaB1vAfN_t^?Og>jQPs e@s2Hr 1|1,7GB"MD/1?y2 grzSp]PR:L* ypjȜw&PV߶!eZ>:\qphć8ʋr@ f_eߣQ3 G 9j JD7.rQ(Dmd6<<LiTn?kr A- 8R-x7~4#c=Rǁ:WA\{\4_W18v׵$,@ldž q l؏48#lkXqF$TMM&1}͕lBROE!h0(2HǀF 5͘֌!TCRpga6e2J`Ŝ8q:b~]*2( FzTS 0ǜrA#U`D¾^AxymL%w!S~kW Mt\9/E:aq`.3h*rrZa5/a %AkG.˺UF6fia=F)^CߤFͫD82衫3^׆hg k /S&0WgW_K,z90\-Z&em&kTk#Lmpu [1VzL EǾ8gm{. _8@_ռW2IhĠufXf4sU*!@j9aP[~ZU0%is[#YۚRUg6Jf*rfT;e0_ܰd02+S'ZnhcE ϲj n+ ;0AΨPrmF"mĨ"]ldXJ9VTﱉ/n0~'[Ax(g'<Dpehn+ %^xĈ/t<:t Q.ɓ<?\p884@d]5JTB ř[]b̀Wur̂41W|]LX v !Mگ¦oFaX/n yNyTJL%I{}piٽЂ,u ֖G 9݀Snєop'wA-^*)$g$evïYѵvnTQ$nղ?0vYvT$:`Gia4% :Yº.iЌdc!%اw=-ؼ`ZŢ(լBn~B Nsox8 Z狻B|PF<P^ѓNweneM#kzduxWcDlENðp4v2 {ثld}4ˇ`FɉC WHwIts7O+&. GW95R-ni/^-D?#^rTƆG!51`܇;g.ɭC@"Bx:xd}:?jl;.ʁe5ߪqvdӗA2N=3(5exO~/88(\鋳g=bPH%zLrS^8R쑹/0g4MamEd2.t.0` E3X@5O&W4T y}ק!dс+QmYyőM['2}xȟb$}l(rZa,Ql\0 Sa;taa= }}U[ NeF ns/=_ 2Eyg7vrEHm*xSޜ=}+śK~\4յrKXtƩ ƜjDs5VsW Oz6+'xM4>'Mc9J3WFԤ(~Ռ?/p-8sA%N|ۖh89XsI^ ȴ覃M׻4xcm۹mM7(X"j]HCz2={tD/Q\YB"v˷ljLST2r#Ǻ4YTff0poʱ45F(4*o7Gj%Bc ]=b™ri,Z+xx.Z@p15b:}wxDF7zS.OVh֛ܰ)p_IPD.VqPO%q/0E|_rwv:!0nm9 l`qRe;m̉̽mչA?i 1_LkȤ@Q7o!ffʻ#¨0 X\z2]OHPK!HtgW 3orator-0.9.8.dist-info/RECORDǖʲY}fpX #!w4g~!m[?M= wIK}c уKFl@zQ}/pBDd@]rqV>e %F 2_zjI9B %{9&ys\*a~{__@8=sf R0}M9fPLG؁׭{ #8&Oeg kkV`AߌTkj]VI2T&Xr%}XmΠy׀y6 KS 1W{pDoDԠ5(qoV8>{a^hB]D7gUD%z&EnI>^ؘ}D@VqR%=M!_5@^HU_`8~۲F>!s"5:'iXRk:Ba1M"*s{o rBBE&4?f 0gTw7o\ͰK[k)^"X} 챱uhp/g(閕I ogJR MZUP{iS3bݷlYm-k s/mfI_U֞:*}F_Zؖ(Y/"[+rki3Ձ ( kq+~?{K2+Pa$x_Ԇ@b<jGu̪f+n;xK`]SԆj,_|܎nWո&gd SφKC봃Iz)rt[{ԉ[ׇC0f ׇ}t=lB+|ҋyruTB)9J[ P??r4~F6 %_lZݡ{߉4?n2﷎)!!-һ?/cJF?hhJm=M9Vg9 F7B3'Μ2 HvB٫]M I *5ebGmh8m-iyξPDZǿk@Ci}x1 b~*H@[Oi?5;LtVФРv,PU 66|ܣ<[xз ˽bg*wq*[ZbcVQpGG_Q\yr14)a(?|B-#3u u/Ohata<\d%n*0GPNr1#;?Jfq8I6R:y bd b-amrNJcQ&McAqkS1Ed8, H[mtK)_^ϡaY7{wD9i8-^J1R PoV~n>+%i)4ppŏ% |KL@fwm^U5O~֕)x ̎\]:pc#wd-kJ/,K~;5dv\9.ĹdNKVtk6a' .J;]]IJ.DyN_Ҁ39mm+~?;T 9EłsZ.̜&' 6bG @HCՋױ $|==nt85mßFs?PWcsUIrO6,i=<w`;@gfiaδ d eXڅ/0. d~m- DŚYLUAk]S/9iX[w|$v(~<R|x8LZ\MD$V{t~J߄qW9[xlhN8?4ᡍ&gٙGӐ;v|zg[J+=ҍrח)\+W_l-9@ >Jh!#kћZ9@ֶnu8kZ:G6xKV軸"_[+qQ&yCȿ7N HW׽5Rz>0#.enkUO'K)4CZ`d2f&ڐHd/Rh^ح~րC';րz*)cBUQ;՛c7U7`#SĩBjcqmʽR؟#GoESłZ86_.U.\ {Q58 0!jZe#nAZ{=h㦏9, v&TRUTd sb ϓ{7äWqtzH~ثy|nu;mvsPu R@71l۔R;+ SE1N$܌ҊC({CG=MEV6Jog!/X91[9`ng&/9 =Mi:)2S'S0\1x`%'>˨A8/hחpUBy$pr)LiE΁-r>s/z5Q~P0MfwVs+ws} ^?o";k$*ciS,   V3ܦyc+5b0هxR= wÔ_du'|؞..OV6_qxw{gzr ;_slY\;kVq&#6䔭#$&ЉC5*rw޽?2Ԗ|".Bn-^3d2#':j͈7akY{C5c=XIHh5Ŏ#Jy*&cnDC|ob`^<| x6]H༨\Mi cc>X\]zMGG"{-w~UӵgŔ4 CA@uչ!)ۺfrzIyKf}!i6>ly %/UꦲkJE1&1|AP9BOͳ|(r(P4LJz4ivF#;?ͫ&zLѮnգBl (y`4 R]T%T6Qe鿃?{j7GVuZ0֚&9fi|7ϾP֮_$Rt8]q[e#{Tmv&oVh޾&~?=7$LH8Xrib?V5 Pܺ|^/ߟ EChvS DXZi ~֪C;m?dOgQJl兲~92Bv>_|iև70(:#xt0\W㜐&V=ǀbG]M=eD"1A˃ Xl-^ռN7v+s޲!N(/gufSPev0DC^>hOB9΁Tbb'YI_;;(.}I=źgVXG` gGt'[,,S-*>hG42(Zdޥp$Zfyw4ږB &B*R)6/5N)/lQ8^NI@]):`35n I Z~!^97Y*Chj`D? \O9=3oG&G}(bk4"ea')mco7_ d H[ 6om"O\?_Եe4ǥ.4D `*%(h5|OqvME7Зn9`ɖns|H2GWSZSY/䋰@[* go3RRXwc KrW7S_OBwY岭v$9ʜ~b.็d:e^h^-dO64Coa!O_y29 [7?RAB]3a}4ms6r}\i.Ww͑%'HƣNpyӍ/`Pq}>hE4@y&Ֆu통+OiU |N{o33$uv-gBAh5HUǷ~MXPq#gDWWxVK)1?tBdKO7waQ| R8TԫjL)ї41'z6~g$x)&O mStEO3`KnZ?ۿgAb jVH K>eV?=X lBk͎ޅ/?]5\)-;SQdMO ^q:V~/>,ڽ9 5,1ŚV{=хG3U Ҿh>*۸/k&P%ëٝڒf%P]U46-:/ܧxtݬ]Ѳڎ8-!Xe!"Ovw|lۦ^cCqŞ#Q̹AiX7h2',(.toąĮ1P]lɖ>$&g_~U"#aI4G9|b10VҭWhH-uCY-׺f 9'1–u+'^yqT,ލ)Z@Du|׽{Pp.f??ޮ*a?٬ @Uh!($sڏg9CDÌ&xss yyorator/orm/relations/__init__.pyPK! m@"*{orator/orm/relations/belongs_to.pyPK!\NZZZZ':orator/orm/relations/belongs_to_many.pyPK!I orator/orm/relations/has_many.pyPK!QLkyy((orator/orm/relations/has_many_through.pyPK!'9orator/orm/relations/has_one.pyPK!In!n!'orator/orm/relations/has_one_or_many.pyPK!1ff"(orator/orm/relations/morph_many.pyPK!?LL!s,orator/orm/relations/morph_one.pyPK!sy)/orator/orm/relations/morph_one_or_many.pyPK!T<#FEorator/orm/relations/morph_pivot.pyPK!^" _Iorator/orm/relations/morph_to.pyPK!6q% %^orator/orm/relations/morph_to_many.pyPK!^ rlorator/orm/relations/pivot.pyPK!^A Oxorator/orm/relations/relation.pyPK!sborator/orm/relations/result.pyPK!7rSSKorator/orm/relations/wrapper.pyPK!__ۖorator/orm/scopes/__init__.pyPK!~t]]uorator/orm/scopes/scope.pyPK!Ghh" orator/orm/scopes/soft_deleting.pyPK!vh;w4w4orator/orm/utils.pyPK!7:ssZorator/pagination/__init__.pyPK!$orator/pagination/base.pyPK!jR_6{ { + orator/pagination/length_aware_paginator.pyPK!"orator/pagination/paginator.pyPK!ȃ;;orator/query/__init__.pyPK!?(êê"orator/query/builder.pyPK!幋orator/query/expression.pyPK![,!9orator/query/grammars/__init__.pyPK!H`66 Borator/query/grammars/grammar.pyPK!xO &]orator/query/grammars/mysql_grammar.pyPK!x)orator/query/grammars/postgres_grammar.pyPK!AXX'orator/query/grammars/sqlite_grammar.pyPK!QQr orator/query/join_clause.pyPK!/eE#orator/query/processors/__init__.pyPK!N*orator/query/processors/mysql_processor.pyPK!v-^orator/query/processors/postgres_processor.pyPK!M$2orator/query/processors/processor.pyPK!j]b+%orator/query/processors/sqlite_processor.pyPK!7iH&orator/schema/__init__.pyPK!{lLL'orator/schema/blueprint.pyPK!5""torator/schema/builder.pyPK!DH"Eorator/schema/grammars/__init__.pyPK!0E&&!Sorator/schema/grammars/grammar.pyPK!ꩩ 'orator/schema/grammars/mysql_grammar.pyPK!l333*Jorator/schema/grammars/postgres_grammar.pyPK!1 1 (orator/schema/grammars/sqlite_grammar.pyPK!o&ET< orator/schema/mysql_builder.pyPK!coE orator/schema/schema.pyPK!ݯ44 orator/seeds/__init__.pyPK!^j orator/seeds/seeder.pyPK!Z` orator/seeds/stubs.pyPK!H<<^ orator/support/__init__.pyPK!^NUyy orator/support/collection.pyPK!akc orator/support/fluent.pyPK!TW W M" orator/support/grammar.pyPK!;L L , orator/utils/__init__.pyPK!7:??!]8 orator/utils/command_formatter.pyPK!ֈJ orator/utils/helpers.pyPK!"[M orator/utils/qmarker.pyPK!( wssP orator/utils/url.pyPK!H ~#:F'n orator-0.9.8.dist-info/entry_points.txtPK!P&&o orator-0.9.8.dist-info/LICENSEPK!H,kXZs orator-0.9.8.dist-info/WHEELPK!Hn: Vt orator-0.9.8.dist-info/METADATAPK!HtgW 3 orator-0.9.8.dist-info/RECORDPK&-