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 [], )
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)
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 [], )
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)
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, )
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, )
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
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)
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)
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 [], )