def create_entity_from_type_dict(
    class_name: str,
    identifier_column_name: str,
    type_dict: Dict,
    foreign_keys: Set[Tuple[str, str, str]] = set(),
    indexes: Set[str] = set(),
    operations: Optional[Set[OperationOption]] = None,
    relationships: Optional[List[Relationship]] = None,
    table_name: Optional[str] = None,
    uniques: Optional[List[List[str]]] = None,
    api_paths: Optional[List[APIPath]] = None,
    model_alias: Optional[ImportAlias] = None,
    additional_properties: Optional[List[AdditionalProperty]] = None,
) -> Entity:
    columns = []
    foreign_keys_dict = {}
    for fk_key, fk_value, fk_type in foreign_keys:
        foreign_keys_dict[fk_key] = ForeignKeyRelationship(
            f'{pythonize(fk_value)}', string_to_type_option(fk_type))
    identifier_column = None
    for k, v in type_dict.items():
        nullable = v.endswith('?')
        v = v.replace('?', '')
        type_option = string_to_type_option(v)
        foreign_key = foreign_keys_dict[k] if k in foreign_keys_dict else None
        index = k in indexes
        if pythonize(k) == pythonize(identifier_column_name):
            identifier_column = create_identifier_column(k, type_option)
        else:
            column = create_column(name=k,
                                   type_option=type_option,
                                   foreign_key_relationship=foreign_key,
                                   index=index,
                                   nullable=nullable,
                                   identifier=False)
            columns.append(column)

    if identifier_column is None:
        raise GenyratorError('Entity must have an identifier column')

    return create_entity(
        class_name=class_name,
        identifier_column=identifier_column,
        columns=columns,
        operations=operations if operations is not None else all_operations,
        relationships=relationships if relationships else [],
        table_name=table_name,
        uniques=uniques if uniques else [],
        api_paths=api_paths,
        model_alias=model_alias,
        additional_properties=additional_properties
        if additional_properties is not None else [],
    )
Ejemplo n.º 2
0
def create_column(
    name: str,
    type_option: TypeOption,
    index: bool = False,
    nullable: bool = True,
    identifier: bool = False,
    foreign_key_relationship: Optional[str] = None,
    display_name: Optional[str] = None,
) -> Column:
    if identifier is True:
        constructor = IdentifierColumn
    elif foreign_key_relationship is not None:
        constructor = ForeignKey
    else:
        constructor = Column
    args = {
        "python_name": pythonize(name),
        "class_name": to_class_name(name),
        "json_property_name": to_json_case(name),
        "display_name": display_name if display_name else humanize(name),
        "type_option": type_option,
        "sqlalchemy_type": type_option_to_sqlalchemy_type(type_option),
        "python_type": type_option_to_python_type(type_option),
        "restplus_type": type_option_to_restplus_type(type_option),
        "default": type_option_to_default_value(type_option),
        "index": index,
        "nullable": nullable,
    }
    if foreign_key_relationship is not None:
        args["relationship"] = foreign_key_relationship
    return constructor(**args)
Ejemplo n.º 3
0
def create_entity(
        class_name:            str,
        identifier_column:     IdentifierColumn,
        columns:               List[Column],
        relationships:         List[Relationship]=list(),
        uniques:               List[List[str]]=list(),
        operations:            Optional[Set[OperationOption]]=None,
        display_name:          Optional[str]=None,
        table_name:            Optional[str]=None,
        plural:                Optional[str]=None,
        dashed_plural:         Optional[str]=None,
        resource_namespace:    Optional[str]=None,
        resource_path:         Optional[str]=None,
        api_paths:             Optional[APIPaths]=None,
        model_alias:           Optional[ImportAlias]=None,
        additional_properties: Optional[List[AdditionalProperty]]=None
) -> Entity:
    operations = operations if operations is not None else all_operations
    python_name = pythonize(class_name)
    columns = [identifier_column, *columns]
    uniques = [[identifier_column.python_name], *uniques]
    max_property_length = _calculate_max_property_length(
        identifier_column, columns, relationships
    )
    return Entity(
        class_name=class_name,
        python_name=python_name,
        identifier_column=identifier_column,
        columns=columns,
        max_property_length=max_property_length,
        relationships=relationships,
        display_name=display_name if display_name is not None else humanize(class_name),
        table_name=table_name if table_name is not None else None,
        uniques=uniques,
        plural=plural if plural is not None else pluralize(python_name),
        dashed_plural=dashed_plural if dashed_plural is not None else dasherize(pluralize(python_name)),
        resource_namespace=resource_namespace if resource_namespace is not None else pluralize(python_name),
        resource_path=resource_path if resource_path is not None else '/',
        dashed_name=dasherize(python_name),
        table_args=_convert_uniques_to_table_args_string(uniques),
        operations=operations if operations is not None else all_operations,
        api_paths=api_paths if api_paths is not None else [],
        supports_put=OperationOption.create_with_id in operations,
        supports_get_one=OperationOption.get_one in operations,
        supports_get_all=OperationOption.get_all in operations,
        supports_post=OperationOption.create_without_id in operations,
        supports_patch=OperationOption.patch in operations,
        supports_delete_one=OperationOption.delete_one in operations,
        supports_delete_all=OperationOption.delete_all in operations,
        model_alias=model_alias,
        additional_properties=additional_properties if additional_properties is not None else [],
    )
Ejemplo n.º 4
0
def create_api_path(
        joined_entities: List[str],
        route:           str,
        endpoint:        Optional[str]=None,
        class_name:      Optional[str]=None,
        property_name:   Optional[str]=None,
) -> APIPath:
    python_name = pythonize(joined_entities[-1])
    property_name = property_name if property_name else to_json_case(python_name)
    return APIPath(joined_entities, route,
                   endpoint if endpoint else '-'.join(joined_entities),
                   class_name=class_name if class_name else to_class_name(python_name),
                   python_name=python_name, property_name=property_name)
Ejemplo n.º 5
0
def create_entity_from_type_dict(
        class_name:             str,
        identifier_column_name: str,
        type_dict:              Dict,
        foreign_keys:           Set[Tuple[str, str]]=set(),
        indexes:                Set[str]=set(),
        operations:             Optional[Set[OperationOption]]=None,
        relationships:          Optional[List[Relationship]]=None,
        table_name:             Optional[str]=None,
        uniques:                Optional[List[List[str]]]=None,
        api_paths:              Optional[APIPaths]=None,
        model_alias:            Optional[ImportAlias]=None,
) -> Entity:
    columns = []
    foreign_keys_dict = {}
    for fk_key, fk_value in foreign_keys:
        foreign_keys_dict[fk_key] = '{table}.{fk_column}'.format(
            table=fk_value, fk_column=pythonize(fk_key)
        )
    identifier_column = None
    for k, v in type_dict.items():
        nullable = v.endswith('?')
        v = v.replace('?', '')
        type_option = string_to_type_option(v)
        foreign_key = foreign_keys_dict[k] if k in foreign_keys_dict else None
        index = k in indexes
        if k == identifier_column_name:
            identifier_column = create_identifier_column(k, type_option)
        else:
            column = create_column(
                name=k,
                type_option=type_option,
                foreign_key_relationship=foreign_key,
                index=index,
                nullable=nullable,
                identifier=False
            )
            columns.append(column)
    return create_entity(
        class_name=class_name,
        identifier_column=identifier_column,
        columns=columns,
        operations=operations if operations is not None else all_operations,
        relationships=relationships if relationships else [],
        table_name=table_name,
        uniques=uniques if uniques else [],
        api_paths=api_paths,
        model_alias=model_alias,
    )
Ejemplo n.º 6
0
def create_relationship(
    target_entity_class_name: str,
    nullable: bool,
    lazy: bool,
    join: JoinOption,
    join_table: Optional[str] = None,
    property_name: Optional[str] = None,
) -> Relationship:
    target_entity_python_name = pythonize(target_entity_class_name)
    return Relationship(
        python_name=target_entity_python_name,
        target_entity_class_name=target_entity_class_name,
        target_entity_python_name=target_entity_python_name,
        property_name=property_name
        if property_name else target_entity_python_name,
        nullable=nullable,
        lazy=lazy,
        join=join,
        join_table=str(join_table) if join_table else None,
    )
Ejemplo n.º 7
0
def create_column(
    name: str,
    type_option: TypeOption,
    index: bool = False,
    nullable: bool = True,
    identifier: bool = False,
    display_name: Optional[str] = None,
    alias: Optional[str] = None,
    foreign_key_relationship: Optional[ForeignKeyRelationship] = None,
    faker_method: Optional[str] = None,
    faker_options: Optional[str] = None,
    sqlalchemy_options: Optional[Dict[str, str]] = None,
) -> Union[Column, IdentifierColumn, ForeignKey]:
    """Return a column to be attached to an entity

    Args:
        name:        The property name on the SQLAlchemy model

        type_option: The type of the column.

        index:       Whether to create a database index for the column.

        nullable:    Whether to allow the column to be nullable in the database.

        identifier:  If set to True all other args will be ignored and this will
                     created as an identifier column. See: create_identifier_column

        display_name: Human readable name intended for use in UIs.

        alias:        Column name to be used the database.

        foreign_key_relationship: The entity this column relates. If this is not
                                  None the result will be a `ForeignKey`.

        faker_method: The method to pass to Faker to provide fixture data for this column.
                      If this column is not nullable, defaults to the constructor for the type
                      of this column.

        sqlalchemy_options: Pass additional keyword arguments to the SQLAlchemy column object.
    """
    if identifier is True:
        constructor: Type[Column] = IdentifierColumn
    elif foreign_key_relationship is not None:
        constructor = ForeignKey
    else:
        constructor = Column

    if faker_method is None and nullable is False:
        faker_method = type_option_to_faker_method(type_option)

    if sqlalchemy_options is None:
        sqlalchemy_options = {}

    args = {
        "python_name": pythonize(name),
        "class_name": to_class_name(name),
        "json_property_name": to_json_case(name),
        "display_name": display_name if display_name else humanize(name),
        "type_option": type_option,
        "sqlalchemy_type": type_option_to_sqlalchemy_type(type_option),
        "python_type": type_option_to_python_type(type_option),
        "restplus_type": type_option_to_restplus_type(type_option),
        "default": type_option_to_default_value(type_option),
        "index": index,
        "nullable": nullable,
        "alias": alias,
        "faker_method": faker_method,
        "faker_options": faker_options,
        "sqlalchemy_options": list(sqlalchemy_options.items()),
    }
    if foreign_key_relationship is not None:
        args['relationship'] = '{}.{}'.format(
            pythonize(foreign_key_relationship.target_entity), 'id')
        args['target_restplus_type'] = type_option_to_restplus_type(
            foreign_key_relationship.target_entity_identifier_column_type)
        args['foreign_key_sqlalchemy_options'] = list(
            foreign_key_relationship.sqlalchemy_options.items())
    return constructor(**args)  # type: ignore
Ejemplo n.º 8
0
def create_relationship(
    target_entity_class_name: str,
    nullable: bool,
    lazy: bool,
    join: JoinOption,
    source_identifier_column_name: str,
    *,
    source_foreign_key_column_name: Optional[str] = None,
    key_alias_in_json: Optional[str] = None,
    join_table: Optional[str] = None,
    target_identifier_column_name: Optional[str] = None,
    target_foreign_key_column_name: Optional[str] = None,
    property_name: Optional[str] = None,
    secondary_join_name: Optional[str] = None,
) -> Relationship:
    """Return a relationship between two entities

    Args:
        target_entity_class_name:  The entity this relationship is pointing to
                                   (ie. not the entity it is defined on).

        nullable: Early validation for whether the target column is nullable.

        lazy: If False the target entity is embedded in the JSON response.

        join: Whether the relationship is 1-to-1 or 1-to-many. If 1-to-1 the property
              will be scalar, if 1-to-many it will be a list.

        source_identifier_column_name: The identifier column for the entity
                                       this relationship starts from. This is
                                       *not* the join key.

        source_foreign_key_column_name: The foreign key property on the entity
                                        this relationship starts from. This will
                                        be None for 1-to-many relationship.

        key_alias_in_json: The name used for this relationship when it appears in JSON.
                           This needs to be unique for a model.
                           Has sensible default.

        join_table: The table name to join through to the target entity.
                    This is usually only needed for many-to-many relationships.

        target_identifier_column_name: The identifier column of the target entity. (deprecated)

        target_foreign_key_column_name: The column name of the foreign key on the
                                        target model. This is usually only needed if
                                        there are multiple routes to the other entity.

        property_name: The property name used on the SQLAlchemy model.

        secondary_join_name: The column name of the secondary foreign key on the
                             intermediary join table when doing a many-to-many join
                             on a self-referential many-to-many relationship.
                             See: https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#self-referential-many-to-many-relationship
    """
    if source_foreign_key_column_name is not None:
        if target_foreign_key_column_name is not None:
            raise Exception(
                'Cannot provide both source and target foreign key columns')
        if join != JoinOption.to_one:
            raise Exception(
                'Can only provide source foreign key column on to-one relationships'
            )

    target_entity_python_name = pythonize(target_entity_class_name)
    property_name = property_name if property_name is not None else target_entity_python_name
    relationship = Relationship(
        python_name=target_entity_python_name,
        target_entity_class_name=target_entity_class_name,
        target_entity_python_name=target_entity_python_name,
        target_foreign_key_column_name=target_foreign_key_column_name,
        source_identifier_column_name=source_identifier_column_name,
        source_foreign_key_column_name=source_foreign_key_column_name,
        property_name=property_name,
        json_property_name=to_json_case(property_name),
        key_alias_in_json=key_alias_in_json
        if key_alias_in_json is not None else target_identifier_column_name,
        nullable=nullable,
        lazy=lazy,
        join=join,
        secondary_join_name=secondary_join_name,
    )
    if join_table is None:
        target_identifier_column_name = pythonize(target_identifier_column_name) \
            if target_identifier_column_name else None
        return RelationshipWithoutJoinTable(
            **{
                **{
                    'target_identifier_column_name': target_identifier_column_name,
                    **relationship.__dict__
                }
            })
    join_table = str(join_table) if join_table else None

    properties = relationship.__dict__
    properties.update({
        'join_table': join_table,
        'join_table_class_name': to_class_name(join_table),
    })
    return RelationshipWithJoinTable(**properties)
Ejemplo n.º 9
0
def create_relationship(
    target_entity_class_name: str,
    nullable: bool,
    lazy: bool,
    join: JoinOption,
    source_identifier_column_name: str,
    *,
    source_foreign_key_column_name: Optional[str] = None,
    key_alias_in_json: Optional[str] = None,
    join_table: Optional[str] = None,
    target_identifier_column_name: Optional[str] = None,
    target_foreign_key_column_name: Optional[str] = None,
    property_name: Optional[str] = None,
    secondary_join_name: Optional[str] = None,
    passive_deletes: Optional[Union[bool, str]] = None,
    cascade: Optional[str] = None,
    post_update: bool = False,
) -> Relationship:
    """Return a relationship between two entities

    Args:
        target_entity_class_name:  The entity this relationship is pointing to
                                   (ie. not the entity it is defined on).

        nullable: Early validation for whether the target column is nullable.

        lazy: If False the target entity is embedded in the JSON response.

        join: Whether the relationship is 1-to-1 or 1-to-many. If 1-to-1 the property
              will be scalar, if 1-to-many it will be a list.

        source_identifier_column_name: The identifier column for the entity
                                       this relationship starts from. This is
                                       *not* the join key.

        source_foreign_key_column_name: The foreign key property on the entity
                                        this relationship starts from. This will
                                        be None for 1-to-many relationship.

        key_alias_in_json: The name used for this relationship when it appears in JSON.
                           This needs to be unique for a model.
                           Has sensible default.

        join_table: The table name to join through to the target entity.
                    This is usually only needed for many-to-many relationships.

        target_identifier_column_name: The identifier column of the target entity. (deprecated)

        target_foreign_key_column_name: The column name of the foreign key on the
                                        target model. This is usually only needed if
                                        there are multiple routes to the other entity.

        property_name: The property name used on the SQLAlchemy model.

        secondary_join_name: The column name of the secondary foreign key on the
                             intermediary join table when doing a many-to-many join
                             on a self-referential many-to-many relationship.
                             See: https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#self-referential-many-to-many-relationship

        passive_deletes: Option to prevent SQLAlchemy from cascading to target objects on delete.  Valid options are True|False|'all'

        cascade: SQLAlchemy ORM Cascading option.  See: https://docs.sqlalchemy.org/en/14/orm/cascades.html

        post_update: Set this to have sqlalchemy set relationships after create or before delete.  Useful for items
                     with circular relationships.  See: https://docs.sqlalchemy.org/en/14/orm/relationship_persistence.html

    """
    if source_foreign_key_column_name is not None:
        if target_foreign_key_column_name is not None:
            raise GenyratorError(
                'Cannot provide both source and target foreign key columns')
        if join != JoinOption.to_one:
            raise GenyratorError(
                'Can only provide source foreign key column on to-one relationships'
            )

    target_entity_python_name = pythonize(target_entity_class_name)
    property_name = property_name if property_name is not None else target_entity_python_name

    if key_alias_in_json is None:
        if target_identifier_column_name is None:
            raise GenyratorError(
                'Must have a key_alias_in_json or target_idenfitier_column_name'
            )
        key_alias_in_json = target_identifier_column_name

    if isinstance(passive_deletes, str) and passive_deletes != 'all':
        passive_deletes = None

    relationship = Relationship(
        python_name=target_entity_python_name,
        target_entity_class_name=target_entity_class_name,
        target_entity_python_name=target_entity_python_name,
        target_foreign_key_column_name=target_foreign_key_column_name,
        source_identifier_column_name=source_identifier_column_name,
        source_foreign_key_column_name=source_foreign_key_column_name,
        property_name=property_name,
        json_property_name=to_json_case(property_name),
        key_alias_in_json=key_alias_in_json,
        nullable=nullable,
        lazy=lazy,
        join=join,
        secondary_join_name=secondary_join_name,
        passive_deletes=passive_deletes,
        cascade=cascade,
        post_update=post_update,
    )
    if join_table is None:
        properties = relationship.__dict__
        properties.update({
            'target_identifier_column_name':
            pythonize(target_identifier_column_name)
            if target_identifier_column_name else None,
        })
        return RelationshipWithoutJoinTable(**properties)
    else:
        properties = relationship.__dict__
        properties.update({
            'join_table': join_table,
            'join_table_class_name': to_class_name(join_table),
        })
        return RelationshipWithJoinTable(**properties)
Ejemplo n.º 10
0
def create_entity(
    class_name: str,
    identifier_column: IdentifierColumn,
    columns: List[Column],
    relationships: List[Relationship] = list(),
    uniques: List[List[str]] = list(),
    operations: Optional[Set[OperationOption]] = None,
    display_name: Optional[str] = None,
    table_name: Optional[str] = None,
    plural: Optional[str] = None,
    dashed_plural: Optional[str] = None,
    resource_namespace: Optional[str] = None,
    resource_path: Optional[str] = None,
    api_paths: Optional[List[APIPath]] = None,
    model_alias: Optional[ImportAlias] = None,
    additional_properties: Optional[List[AdditionalProperty]] = None
) -> Entity:
    """Return a fully configured Entity

    The returned Entity is configured with columns and relationships.

    Args:
        class_name:          The class name (eg BookClub) used for the SQLAlchemy
                             and RESTPlus models

        identifier_column:   The column used to identify the entity in the URL. Note;
                             this is not the primary key of the SQLAlchemy model and therefore
                             is not used for joins.

        columns:             All columns except for the internally generated primary key.
                             This must includes the identifier column and any columns
                             required for relationships.

        relationships:       The relationships between entities. This means just SQLAlchemy
                             relationships unless the relationship is non-lazy in which case
                             it will also appear in the JSON response.

        uniques:             A list of list of column names that require unique indexes.
                             The identifier column does not need to appear in here.

        operations:          HTTP actions which should be generated for this entity.

        display_name:        Human readable name (eg Book Club). Has sensible default.

        table_name:          The SQLAlchemy table. Has sensible default.

        plural:              The plural form of the entity name used in method names
                             (eg book_clubs). Has sensible default.

        dashed_plural:       The plural form of the entity name used in URIs and
                             RESTPlus resource IDs (eg book-clubs). Has sensible default.

        resource_namespace:  RESTPlus namespace resource name for entity (eg BookClub). Has sensible default.

        resource_path:       RESTPlus namespace path. Has sensible default.

        api_paths:           List of class names for which RESTPlus routes should
                             be created. A relationship must exist for the path.
                             This is only needed for lazy relationships.

        model_alias:         Override the SQLAlchemy model used in the RESTPlus routes.
                             Use this if you want to extend the generated SQLAlchemy model.

        additional_properties: Key value pairs to be added to the SQLAlchemy model.
                               They will end up in the model as `key = value`.
    """
    operations = operations if operations is not None else all_operations
    python_name = pythonize(class_name)
    columns = [identifier_column, *columns]
    if [identifier_column.python_name] not in uniques:
        uniques = [[identifier_column.python_name], *uniques]
    max_property_length = _calculate_max_property_length(
        identifier_column, columns, relationships)
    return Entity(
        class_name=class_name,
        python_name=python_name,
        identifier_column=identifier_column,
        columns=columns,
        max_property_length=max_property_length,
        relationships=relationships,
        display_name=display_name
        if display_name is not None else humanize(class_name),
        table_name=table_name if table_name is not None else None,
        uniques=uniques,
        plural=plural if plural is not None else pluralize(python_name),
        dashed_plural=dashed_plural
        if dashed_plural is not None else dasherize(pluralize(python_name)),
        resource_namespace=resource_namespace
        if resource_namespace is not None else pluralize(python_name),
        resource_path=resource_path if resource_path is not None else '/',
        dashed_name=dasherize(python_name),
        table_args=_convert_uniques_to_table_args_string(uniques),
        operations=operations if operations is not None else all_operations,
        api_paths=api_paths if api_paths is not None else [],
        supports_put=OperationOption.create_with_id in operations,
        supports_get_one=OperationOption.get_one in operations,
        supports_get_all=OperationOption.get_all in operations,
        supports_post=OperationOption.create_without_id in operations,
        supports_patch=OperationOption.patch in operations,
        supports_delete_one=OperationOption.delete_one in operations,
        supports_delete_all=OperationOption.delete_all in operations,
        model_alias=model_alias,
        additional_properties=additional_properties
        if additional_properties is not None else [],
    )