ariatosca-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mxm...@apache.org
Subject [06/11] incubator-ariatosca git commit: ARIA-44 Merge parser and storage models
Date Thu, 09 Feb 2017 14:49:09 GMT
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/2b3276bb/aria/storage/modeling/model.py
----------------------------------------------------------------------
diff --git a/aria/storage/modeling/model.py b/aria/storage/modeling/model.py
new file mode 100644
index 0000000..74e419d
--- /dev/null
+++ b/aria/storage/modeling/model.py
@@ -0,0 +1,213 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from sqlalchemy.ext.declarative import declarative_base
+
+from . import (
+    template_elements,
+    instance_elements,
+    orchestrator_elements,
+    elements,
+    structure,
+)
+
+__all__ = (
+    'DB',
+    'Parameter',
+    'MappingTemplate',
+    'InterfaceTemplate',
+    'OperationTemplate',
+    'ServiceTemplate',
+    'NodeTemplate',
+    'GroupTemplate',
+    'ArtifactTemplate',
+    'PolicyTemplate',
+    'GroupPolicyTemplate',
+    'GroupPolicyTriggerTemplate',
+    'RequirementTemplate',
+    'CapabilityTemplate',
+
+    'Mapping',
+    'Substitution',
+    'ServiceInstance',
+    'Node',
+    'Relationship',
+    'Artifact',
+    'Group',
+    'Interface',
+    'Operation',
+    'Capability',
+    'Policy',
+    'GroupPolicy',
+    'GroupPolicyTrigger',
+
+    'Execution',
+    'ServiceInstanceUpdate',
+    'ServiceInstanceUpdateStep',
+    'ServiceInstanceModification',
+    'Plugin',
+    'Task'
+)
+
+DB = declarative_base(cls=structure.ModelIDMixin)
+
+# pylint: disable=abstract-method
+
+# region elements
+
+
+class Parameter(elements.ParameterBase, DB):
+    pass
+
+# endregion
+
+# region template models
+
+
+class MappingTemplate(DB, template_elements.MappingTemplateBase):
+    pass
+
+
+class SubstitutionTemplate(DB, template_elements.SubstitutionTemplateBase):
+    pass
+
+
+class InterfaceTemplate(DB, template_elements.InterfaceTemplateBase):
+    pass
+
+
+class OperationTemplate(DB, template_elements.OperationTemplateBase):
+    pass
+
+
+class ServiceTemplate(DB, template_elements.ServiceTemplateBase):
+    pass
+
+
+class NodeTemplate(DB, template_elements.NodeTemplateBase):
+    pass
+
+
+class GroupTemplate(DB, template_elements.GroupTemplateBase):
+    pass
+
+
+class ArtifactTemplate(DB, template_elements.ArtifactTemplateBase):
+    pass
+
+
+class PolicyTemplate(DB, template_elements.PolicyTemplateBase):
+    pass
+
+
+class GroupPolicyTemplate(DB, template_elements.GroupPolicyTemplateBase):
+    pass
+
+
+class GroupPolicyTriggerTemplate(DB, template_elements.GroupPolicyTriggerTemplateBase):
+    pass
+
+
+class RequirementTemplate(DB, template_elements.RequirementTemplateBase):
+    pass
+
+
+class CapabilityTemplate(DB, template_elements.CapabilityTemplateBase):
+    pass
+
+
+# endregion
+
+# region instance models
+
+class Mapping(DB, instance_elements.MappingBase):
+    pass
+
+
+class Substitution(DB, instance_elements.SubstitutionBase):
+    pass
+
+
+class ServiceInstance(DB, instance_elements.ServiceInstanceBase):
+    pass
+
+
+class Node(DB, instance_elements.NodeBase):
+    pass
+
+
+class Relationship(DB, instance_elements.RelationshipBase):
+    pass
+
+
+class Artifact(DB, instance_elements.ArtifactBase):
+    pass
+
+
+class Group(DB, instance_elements.GroupBase):
+    pass
+
+
+class Interface(DB, instance_elements.InterfaceBase):
+    pass
+
+
+class Operation(DB, instance_elements.OperationBase):
+    pass
+
+
+class Capability(DB, instance_elements.CapabilityBase):
+    pass
+
+
+class Policy(DB, instance_elements.PolicyBase):
+    pass
+
+
+class GroupPolicy(DB, instance_elements.GroupPolicyBase):
+    pass
+
+
+class GroupPolicyTrigger(DB, instance_elements.GroupPolicyTriggerBase):
+    pass
+
+
+# endregion
+
+# region orchestrator models
+
+class Execution(DB, orchestrator_elements.Execution):
+    pass
+
+
+class ServiceInstanceUpdate(DB, orchestrator_elements.ServiceInstanceUpdateBase):
+    pass
+
+
+class ServiceInstanceUpdateStep(DB, orchestrator_elements.ServiceInstanceUpdateStepBase):
+    pass
+
+
+class ServiceInstanceModification(DB, orchestrator_elements.ServiceInstanceModificationBase):
+    pass
+
+
+class Plugin(DB, orchestrator_elements.PluginBase):
+    pass
+
+
+class Task(DB, orchestrator_elements.TaskBase):
+    pass
+# endregion

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/2b3276bb/aria/storage/modeling/orchestrator_elements.py
----------------------------------------------------------------------
diff --git a/aria/storage/modeling/orchestrator_elements.py b/aria/storage/modeling/orchestrator_elements.py
new file mode 100644
index 0000000..a7ed5e9
--- /dev/null
+++ b/aria/storage/modeling/orchestrator_elements.py
@@ -0,0 +1,461 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Aria's storage.models module
+Path: aria.storage.models
+
+models module holds aria's models.
+
+classes:
+    * Field - represents a single field.
+    * IterField - represents an iterable field.
+    * Model - abstract model implementation.
+    * Snapshot - snapshots implementation model.
+    * Deployment - deployment implementation model.
+    * DeploymentUpdateStep - deployment update step implementation model.
+    * DeploymentUpdate - deployment update implementation model.
+    * DeploymentModification - deployment modification implementation model.
+    * Execution - execution implementation model.
+    * Node - node implementation model.
+    * Relationship - relationship implementation model.
+    * NodeInstance - node instance implementation model.
+    * RelationshipInstance - relationship instance implementation model.
+    * Plugin - plugin implementation model.
+"""
+from collections import namedtuple
+from datetime import datetime
+
+from sqlalchemy import (
+    Column,
+    Integer,
+    Text,
+    DateTime,
+    Boolean,
+    Enum,
+    String,
+    Float,
+    orm,
+)
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.declarative import declared_attr
+
+from aria.orchestrator.exceptions import TaskAbortException, TaskRetryException
+
+from .type import List, Dict
+from .structure import ModelMixin
+
+__all__ = (
+    'ServiceInstanceUpdateStepBase',
+    'ServiceInstanceUpdateBase',
+    'ServiceInstanceModificationBase',
+    'Execution',
+    'PluginBase',
+    'TaskBase'
+)
+
+# pylint: disable=no-self-argument, no-member, abstract-method
+
+
+class Execution(ModelMixin):
+    """
+    Execution model representation.
+    """
+    # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column.
+    __tablename__ = 'execution'
+
+    TERMINATED = 'terminated'
+    FAILED = 'failed'
+    CANCELLED = 'cancelled'
+    PENDING = 'pending'
+    STARTED = 'started'
+    CANCELLING = 'cancelling'
+    FORCE_CANCELLING = 'force_cancelling'
+
+    STATES = [TERMINATED, FAILED, CANCELLED, PENDING, STARTED, CANCELLING, FORCE_CANCELLING]
+    END_STATES = [TERMINATED, FAILED, CANCELLED]
+    ACTIVE_STATES = [state for state in STATES if state not in END_STATES]
+
+    VALID_TRANSITIONS = {
+        PENDING: [STARTED, CANCELLED],
+        STARTED: END_STATES + [CANCELLING],
+        CANCELLING: END_STATES + [FORCE_CANCELLING]
+    }
+
+    @orm.validates('status')
+    def validate_status(self, key, value):
+        """Validation function that verifies execution status transitions are OK"""
+        try:
+            current_status = getattr(self, key)
+        except AttributeError:
+            return
+        valid_transitions = self.VALID_TRANSITIONS.get(current_status, [])
+        if all([current_status is not None,
+                current_status != value,
+                value not in valid_transitions]):
+            raise ValueError('Cannot change execution status from {current} to {new}'.format(
+                current=current_status,
+                new=value))
+        return value
+
+    created_at = Column(DateTime, index=True)
+    started_at = Column(DateTime, nullable=True, index=True)
+    ended_at = Column(DateTime, nullable=True, index=True)
+    error = Column(Text, nullable=True)
+    is_system_workflow = Column(Boolean, nullable=False, default=False)
+    parameters = Column(Dict)
+    status = Column(Enum(*STATES, name='execution_status'), default=PENDING)
+    workflow_name = Column(Text)
+
+    @declared_attr
+    def service_template(cls):
+        return association_proxy('service_instance', 'service_template')
+
+    @declared_attr
+    def service_instance_fk(cls):
+        return cls.foreign_key('service_instance')
+
+    @declared_attr
+    def service_instance(cls):
+        return cls.many_to_one_relationship('service_instance')
+
+    @declared_attr
+    def service_instance_name(cls):
+        return association_proxy('service_instance', cls.name_column_name())
+
+    @declared_attr
+    def service_template_name(cls):
+        return association_proxy('service_instance', 'service_template_name')
+
+    def __str__(self):
+        return '<{0} id=`{1}` (status={2})>'.format(
+            self.__class__.__name__,
+            getattr(self, self.name_column_name()),
+            self.status
+        )
+
+
+class ServiceInstanceUpdateBase(ModelMixin):
+    """
+    Deployment update model representation.
+    """
+    # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column.
+    steps = None
+
+    __tablename__ = 'service_instance_update'
+
+    _private_fields = ['execution_fk', 'deployment_fk']
+
+    created_at = Column(DateTime, nullable=False, index=True)
+    deployment_plan = Column(Dict, nullable=False)
+    deployment_update_node_instances = Column(Dict)
+    deployment_update_deployment = Column(Dict)
+    deployment_update_nodes = Column(List)
+    modified_entity_ids = Column(Dict)
+    state = Column(Text)
+
+    @declared_attr
+    def execution_fk(cls):
+        return cls.foreign_key('execution', nullable=True)
+
+    @declared_attr
+    def execution(cls):
+        return cls.many_to_one_relationship('execution')
+
+    @declared_attr
+    def execution_name(cls):
+        return association_proxy('execution', cls.name_column_name())
+
+    @declared_attr
+    def service_instance_fk(cls):
+        return cls.foreign_key('service_instance')
+
+    @declared_attr
+    def service_instance(cls):
+        return cls.many_to_one_relationship('service_instance')
+
+    @declared_attr
+    def service_instance_name(cls):
+        return association_proxy('service_instance', cls.name_column_name())
+
+    def to_dict(self, suppress_error=False, **kwargs):
+        dep_update_dict = super(ServiceInstanceUpdateBase, self).to_dict(suppress_error)
    #pylint: disable=no-member
+        # Taking care of the fact the DeploymentSteps are _BaseModels
+        dep_update_dict['steps'] = [step.to_dict() for step in self.steps]
+        return dep_update_dict
+
+
+class ServiceInstanceUpdateStepBase(ModelMixin):
+    """
+    Deployment update step model representation.
+    """
+    # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column.
+    __tablename__ = 'service_instance_update_step'
+    _private_fields = ['deployment_update_fk']
+
+    _action_types = namedtuple('ACTION_TYPES', 'ADD, REMOVE, MODIFY')
+    ACTION_TYPES = _action_types(ADD='add', REMOVE='remove', MODIFY='modify')
+    _entity_types = namedtuple(
+        'ENTITY_TYPES',
+        'NODE, RELATIONSHIP, PROPERTY, OPERATION, WORKFLOW, OUTPUT, DESCRIPTION, GROUP, '
+        'POLICY_TYPE, POLICY_TRIGGER, PLUGIN')
+    ENTITY_TYPES = _entity_types(
+        NODE='node',
+        RELATIONSHIP='relationship',
+        PROPERTY='property',
+        OPERATION='operation',
+        WORKFLOW='workflow',
+        OUTPUT='output',
+        DESCRIPTION='description',
+        GROUP='group',
+        POLICY_TYPE='policy_type',
+        POLICY_TRIGGER='policy_trigger',
+        PLUGIN='plugin'
+    )
+
+    action = Column(Enum(*ACTION_TYPES, name='action_type'), nullable=False)
+    entity_id = Column(Text, nullable=False)
+    entity_type = Column(Enum(*ENTITY_TYPES, name='entity_type'), nullable=False)
+
+    @declared_attr
+    def service_instance_update_fk(cls):
+        return cls.foreign_key('service_instance_update')
+
+    @declared_attr
+    def deployment_update(cls):
+        return cls.many_to_one_relationship('service_instance_update',
+                                            backreference='steps')
+
+    @declared_attr
+    def deployment_update_name(cls):
+        return association_proxy('deployment_update', cls.name_column_name())
+
+    def __hash__(self):
+        return hash((getattr(self, self.id_column_name()), self.entity_id))
+
+    def __lt__(self, other):
+        """
+        the order is 'remove' < 'modify' < 'add'
+        :param other:
+        :return:
+        """
+        if not isinstance(other, self.__class__):
+            return not self >= other
+
+        if self.action != other.action:
+            if self.action == 'remove':
+                return_value = True
+            elif self.action == 'add':
+                return_value = False
+            else:
+                return_value = other.action == 'add'
+            return return_value
+
+        if self.action == 'add':
+            return self.entity_type == 'node' and other.entity_type == 'relationship'
+        if self.action == 'remove':
+            return self.entity_type == 'relationship' and other.entity_type == 'node'
+        return False
+
+
+class ServiceInstanceModificationBase(ModelMixin):
+    """
+    Deployment modification model representation.
+    """
+    __tablename__ = 'service_instance_modification'
+    _private_fields = ['deployment_fk']
+
+    STARTED = 'started'
+    FINISHED = 'finished'
+    ROLLEDBACK = 'rolledback'
+
+    STATES = [STARTED, FINISHED, ROLLEDBACK]
+    END_STATES = [FINISHED, ROLLEDBACK]
+
+    context = Column(Dict)
+    created_at = Column(DateTime, nullable=False, index=True)
+    ended_at = Column(DateTime, index=True)
+    modified_nodes = Column(Dict)
+    node_instances = Column(Dict)
+    status = Column(Enum(*STATES, name='deployment_modification_status'))
+
+    @declared_attr
+    def service_instance_fk(cls):
+        return cls.foreign_key('service_instance')
+
+    @declared_attr
+    def service_instance(cls):
+        return cls.many_to_one_relationship('service_instance',
+                                            backreference='modifications')
+
+    @declared_attr
+    def deployment_name(cls):
+        return association_proxy('service_instance', cls.name_column_name())
+
+
+class PluginBase(ModelMixin):
+    """
+    Plugin model representation.
+    """
+    __tablename__ = 'plugin'
+
+    archive_name = Column(Text, nullable=False, index=True)
+    distribution = Column(Text)
+    distribution_release = Column(Text)
+    distribution_version = Column(Text)
+    package_name = Column(Text, nullable=False, index=True)
+    package_source = Column(Text)
+    package_version = Column(Text)
+    supported_platform = Column(Text)
+    supported_py_versions = Column(List)
+    uploaded_at = Column(DateTime, nullable=False, index=True)
+    wheels = Column(List, nullable=False)
+
+
+class TaskBase(ModelMixin):
+    """
+    A Model which represents an task
+    """
+    __tablename__ = 'task'
+    _private_fields = ['node_instance_fk', 'relationship_instance_fk', 'execution_fk']
+
+    @declared_attr
+    def node_fk(cls):
+        return cls.foreign_key('node', nullable=True)
+
+    @declared_attr
+    def node_name(cls):
+        return association_proxy('node', cls.name_column_name())
+
+    @declared_attr
+    def node(cls):
+        return cls.many_to_one_relationship('node')
+
+    @declared_attr
+    def relationship_fk(cls):
+        return cls.foreign_key('relationship', nullable=True)
+
+    @declared_attr
+    def relationship_name(cls):
+        return association_proxy('relationships', cls.name_column_name())
+
+    @declared_attr
+    def relationship(cls):
+        return cls.many_to_one_relationship('relationship')
+
+    @declared_attr
+    def plugin_fk(cls):
+        return cls.foreign_key('plugin', nullable=True)
+
+    @declared_attr
+    def plugin(cls):
+        return cls.many_to_one_relationship('plugin')
+
+    @declared_attr
+    def execution_fk(cls):
+        return cls.foreign_key('execution', nullable=True)
+
+    @declared_attr
+    def execution(cls):
+        return cls.many_to_one_relationship('execution')
+
+    @declared_attr
+    def execution_name(cls):
+        return association_proxy('execution', cls.name_column_name())
+
+    PENDING = 'pending'
+    RETRYING = 'retrying'
+    SENT = 'sent'
+    STARTED = 'started'
+    SUCCESS = 'success'
+    FAILED = 'failed'
+    STATES = (
+        PENDING,
+        RETRYING,
+        SENT,
+        STARTED,
+        SUCCESS,
+        FAILED,
+    )
+
+    WAIT_STATES = [PENDING, RETRYING]
+    END_STATES = [SUCCESS, FAILED]
+
+    RUNS_ON_SOURCE = 'source'
+    RUNS_ON_TARGET = 'target'
+    RUNS_ON_NODE_INSTANCE = 'node_instance'
+    RUNS_ON = (RUNS_ON_NODE_INSTANCE, RUNS_ON_SOURCE, RUNS_ON_TARGET)
+
+    @orm.validates('max_attempts')
+    def validate_max_attempts(self, _, value):                                  # pylint:
disable=no-self-use
+        """Validates that max attempts is either -1 or a positive number"""
+        if value < 1 and value != TaskBase.INFINITE_RETRIES:
+            raise ValueError('Max attempts can be either -1 (infinite) or any positive number.
'
+                             'Got {value}'.format(value=value))
+        return value
+
+    INFINITE_RETRIES = -1
+
+    status = Column(Enum(*STATES, name='status'), default=PENDING)
+
+    due_at = Column(DateTime, default=datetime.utcnow)
+    started_at = Column(DateTime, default=None)
+    ended_at = Column(DateTime, default=None)
+    max_attempts = Column(Integer, default=1)
+    retry_count = Column(Integer, default=0)
+    retry_interval = Column(Float, default=0)
+    ignore_failure = Column(Boolean, default=False)
+
+    # Operation specific fields
+    implementation = Column(String)
+    inputs = Column(Dict)
+    # This is unrelated to the plugin of the task. This field is related to the plugin name
+    # received from the blueprint.
+    plugin_name = Column(String)
+    _runs_on = Column(Enum(*RUNS_ON, name='runs_on'), name='runs_on')
+
+    @property
+    def runs_on(self):
+        if self._runs_on == self.RUNS_ON_NODE_INSTANCE:
+            return self.node
+        elif self._runs_on == self.RUNS_ON_SOURCE:
+            return self.relationship.source_node  # pylint: disable=no-member
+        elif self._runs_on == self.RUNS_ON_TARGET:
+            return self.relationship.target_node  # pylint: disable=no-member
+        return None
+
+    @property
+    def actor(self):
+        """
+        Return the actor of the task
+        :return:
+        """
+        return self.node or self.relationship
+
+    @classmethod
+    def as_node_instance(cls, instance, runs_on, **kwargs):
+        return cls(node=instance, _runs_on=runs_on, **kwargs)
+
+    @classmethod
+    def as_relationship_instance(cls, instance, runs_on, **kwargs):
+        return cls(relationship=instance, _runs_on=runs_on, **kwargs)
+
+    @staticmethod
+    def abort(message=None):
+        raise TaskAbortException(message)
+
+    @staticmethod
+    def retry(message=None, retry_interval=None):
+        raise TaskRetryException(message, retry_interval=retry_interval)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/2b3276bb/aria/storage/modeling/structure.py
----------------------------------------------------------------------
diff --git a/aria/storage/modeling/structure.py b/aria/storage/modeling/structure.py
new file mode 100644
index 0000000..386887e
--- /dev/null
+++ b/aria/storage/modeling/structure.py
@@ -0,0 +1,320 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Aria's storage.structures module
+Path: aria.storage.structures
+
+models module holds aria's models.
+
+classes:
+    * Field - represents a single field.
+    * IterField - represents an iterable field.
+    * PointerField - represents a single pointer field.
+    * IterPointerField - represents an iterable pointers field.
+    * Model - abstract model implementation.
+"""
+
+from sqlalchemy.orm import relationship, backref
+from sqlalchemy.ext import associationproxy
+from sqlalchemy import (
+    Column,
+    ForeignKey,
+    Integer,
+    Text,
+    Table,
+)
+
+from . import utils
+
+
+class Function(object):
+    """
+    An intrinsic function.
+
+    Serves as a placeholder for a value that should eventually be derived
+    by calling the function.
+    """
+
+    @property
+    def as_raw(self):
+        raise NotImplementedError
+
+    def _evaluate(self, context, container):
+        raise NotImplementedError
+
+    def __deepcopy__(self, memo):
+        # Circumvent cloning in order to maintain our state
+        return self
+
+
+class ElementBase(object):
+    """
+    Base class for :class:`ServiceInstance` elements.
+
+    All elements support validation, diagnostic dumping, and representation as
+    raw data (which can be translated into JSON or YAML) via :code:`as_raw`.
+    """
+
+    @property
+    def as_raw(self):
+        raise NotImplementedError
+
+    def validate(self, context):
+        pass
+
+    def coerce_values(self, context, container, report_issues):
+        pass
+
+    def dump(self, context):
+        pass
+
+
+class ModelElementBase(ElementBase):
+    """
+    Base class for :class:`ServiceModel` elements.
+
+    All model elements can be instantiated into :class:`ServiceInstance` elements.
+    """
+
+    def instantiate(self, context, container):
+        raise NotImplementedError
+
+
+class ModelMixin(ModelElementBase):
+
+    @utils.classproperty
+    def __modelname__(cls):                                                             
           # pylint: disable=no-self-argument
+        return getattr(cls, '__mapiname__', cls.__tablename__)
+
+    @classmethod
+    def id_column_name(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def name_column_name(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def _get_cls_by_tablename(cls, tablename):
+        """Return class reference mapped to table.
+
+         :param tablename: String with name of table.
+         :return: Class reference or None.
+         """
+        if tablename in (cls.__name__, cls.__tablename__):
+            return cls
+
+        for table_cls in cls._decl_class_registry.values():
+            if tablename == getattr(table_cls, '__tablename__', None):
+                return table_cls
+
+    @classmethod
+    def foreign_key(cls, table_name, nullable=False):
+        """Return a ForeignKey object with the relevant
+
+        :param table: Unique id column in the parent table
+        :param nullable: Should the column be allowed to remain empty
+        """
+        return Column(Integer,
+                      ForeignKey('{tablename}.id'.format(tablename=table_name), ondelete='CASCADE'),
+                      nullable=nullable)
+
+    @classmethod
+    def one_to_one_relationship(cls, table_name, backreference=None):
+        return relationship(lambda: cls._get_cls_by_tablename(table_name),
+                            backref=backref(backreference or cls.__tablename__, uselist=False))
+
+    @classmethod
+    def many_to_one_relationship(cls,
+                                 parent_table_name,
+                                 foreign_key_column=None,
+                                 backreference=None,
+                                 backref_kwargs=None,
+                                 **kwargs):
+        """Return a one-to-many SQL relationship object
+        Meant to be used from inside the *child* object
+
+        :param parent_class: Class of the parent table
+        :param cls: Class of the child table
+        :param foreign_key_column: The column of the foreign key (from the child table)
+        :param backreference: The name to give to the reference to the child (on the parent
table)
+        """
+        relationship_kwargs = kwargs
+        if foreign_key_column:
+            relationship_kwargs.setdefault('foreign_keys', getattr(cls, foreign_key_column))
+
+        backref_kwargs = backref_kwargs or {}
+        backref_kwargs.setdefault('lazy', 'dynamic')
+        # The following line make sure that when the *parent* is
+        #  deleted, all its connected children are deleted as well
+        backref_kwargs.setdefault('cascade', 'all')
+
+        return relationship(lambda: cls._get_cls_by_tablename(parent_table_name),
+                            backref=backref(backreference or utils.pluralize(cls.__tablename__),
+                                            **backref_kwargs or {}),
+                            **relationship_kwargs)
+
+    @classmethod
+    def relationship_to_self(cls, local_column):
+
+        remote_side_str = '{cls.__name__}.{remote_column}'.format(
+            cls=cls,
+            remote_column=cls.id_column_name()
+        )
+        primaryjoin_str = '{remote_side_str} == {cls.__name__}.{local_column}'.format(
+            remote_side_str=remote_side_str,
+            cls=cls,
+            local_column=local_column)
+        return relationship(cls._get_cls_by_tablename(cls.__tablename__).__name__,
+                            primaryjoin=primaryjoin_str,
+                            remote_side=remote_side_str,
+                            post_update=True)
+
+    @classmethod
+    def many_to_many_relationship(cls, other_table_name, table_prefix, relationship_kwargs=None):
+        """Return a many-to-many SQL relationship object
+
+        Notes:
+        1. The backreference name is the current table's table name
+        2. This method creates a new helper table in the DB
+
+        :param cls: The class of the table we're connecting from
+        :param other_table_name: The class of the table we're connecting to
+        :param table_prefix: Custom prefix for the helper table name and the
+        backreference name
+        """
+        current_table_name = cls.__tablename__
+        current_column_name = '{0}_id'.format(current_table_name)
+        current_foreign_key = '{0}.id'.format(current_table_name)
+
+        other_column_name = '{0}_id'.format(other_table_name)
+        other_foreign_key = '{0}.id'.format(other_table_name)
+
+        helper_table_name = '{0}_{1}'.format(current_table_name, other_table_name)
+
+        backref_name = current_table_name
+        if table_prefix:
+            helper_table_name = '{0}_{1}'.format(table_prefix, helper_table_name)
+            backref_name = '{0}_{1}'.format(table_prefix, backref_name)
+
+        secondary_table = cls.get_secondary_table(
+            cls.metadata,
+            helper_table_name,
+            current_column_name,
+            other_column_name,
+            current_foreign_key,
+            other_foreign_key
+        )
+
+        return relationship(
+            lambda: cls._get_cls_by_tablename(other_table_name),
+            secondary=secondary_table,
+            backref=backref(backref_name),
+            **(relationship_kwargs or {})
+        )
+
+    @staticmethod
+    def get_secondary_table(metadata,
+                            helper_table_name,
+                            first_column_name,
+                            second_column_name,
+                            first_foreign_key,
+                            second_foreign_key):
+        """Create a helper table for a many-to-many relationship
+
+        :param helper_table_name: The name of the table
+        :param first_column_name: The name of the first column in the table
+        :param second_column_name: The name of the second column in the table
+        :param first_foreign_key: The string representing the first foreign key,
+        for example `blueprint.storage_id`, or `tenants.id`
+        :param second_foreign_key: The string representing the second foreign key
+        :return: A Table object
+        """
+        return Table(
+            helper_table_name,
+            metadata,
+            Column(
+                first_column_name,
+                Integer,
+                ForeignKey(first_foreign_key)
+            ),
+            Column(
+                second_column_name,
+                Integer,
+                ForeignKey(second_foreign_key)
+            )
+        )
+
+    def to_dict(self, fields=None, suppress_error=False):
+        """Return a dict representation of the model
+
+        :param suppress_error: If set to True, sets `None` to attributes that
+        it's unable to retrieve (e.g., if a relationship wasn't established
+        yet, and so it's impossible to access a property through it)
+        """
+        res = dict()
+        fields = fields or self.fields()
+        for field in fields:
+            try:
+                field_value = getattr(self, field)
+            except AttributeError:
+                if suppress_error:
+                    field_value = None
+                else:
+                    raise
+            if isinstance(field_value, list):
+                field_value = list(field_value)
+            elif isinstance(field_value, dict):
+                field_value = dict(field_value)
+            elif isinstance(field_value, ModelMixin):
+                field_value = field_value.to_dict()
+            res[field] = field_value
+
+        return res
+
+    @classmethod
+    def _association_proxies(cls):
+        for col, value in vars(cls).items():
+            if isinstance(value, associationproxy.AssociationProxy):
+                yield col
+
+    @classmethod
+    def fields(cls):
+        """Return the list of field names for this table
+
+        Mostly for backwards compatibility in the code (that uses `fields`)
+        """
+        fields = set(cls._association_proxies())
+        fields.update(cls.__table__.columns.keys())
+        return fields - set(getattr(cls, '_private_fields', []))
+
+    def __repr__(self):
+        return '<{__class__.__name__} id=`{id}`>'.format(
+            __class__=self.__class__,
+            id=getattr(self, self.name_column_name()))
+
+
+class ModelIDMixin(object):
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    name = Column(Text, nullable=True, index=True)
+
+    @classmethod
+    def id_column_name(cls):
+        return 'id'
+
+    @classmethod
+    def name_column_name(cls):
+        return 'name'


Mime
View raw message