Example #1
0
def construct(*, schema: types.Schema) -> TableArgs:
    """
    Construct any table args from the object schema.

    Look for x-composite-unique and x-composite-index keys in the schema and construct
    any unique constraints and indexes based on their value.

    Args:
        schema: The schema for the object.

    Returns:
        A tuple with any unique constraints and indexes.

    """
    # Keep track of any table arguments
    table_args: typing.List[typing.Iterable[TableArg]] = []

    # Handle x-composite-unique
    unique_spec = helpers.get_ext_prop(source=schema, name="x-composite-unique")
    if unique_spec is not None:
        table_args.append(factory.unique_factory(spec=unique_spec))
    # Handle x-composite-index
    index_spec = helpers.get_ext_prop(source=schema, name="x-composite-index")
    if index_spec is not None:
        table_args.append(factory.index_factory(spec=index_spec))

    return tuple(itertools.chain.from_iterable(table_args))
Example #2
0
def test_invalid(name, value):
    """
    GIVEN property and invalid value
    WHEN get_ext_prop is called with a source made of the property and value
    THEN MalformedExtensionPropertyError is raised.
    """
    source = {name: value}

    with pytest.raises(exceptions.MalformedExtensionPropertyError):
        helpers.get_ext_prop(source=source, name=name)
def test_composite_index_invalid(value):
    """
    GIVEN value for x-composite-index that has an invalid format
    WHEN get_ext_prop with x-composite-index and the value
    THEN MalformedExtensionPropertyError is raised.
    """
    name = "x-composite-index"
    source = {name: value}

    with pytest.raises(exceptions.MalformedExtensionPropertyError):
        helpers.get_ext_prop(source=source, name=name)
Example #4
0
def _handle_object_reference(*, spec: types.Schema,
                             schemas: types.Schemas) -> types.Schema:
    """
    Determine the foreign key schema for an object reference.

    Args:
        spec: The schema of the object reference.
        schemas: All defined schemas.

    Returns:
        The foreign key schema.

    """
    tablename = helpers.get_ext_prop(source=spec, name="x-tablename")
    if not tablename:
        raise exceptions.MalformedSchemaError(
            "Referenced object is missing x-tablename property.")
    properties = spec.get("properties")
    if properties is None:
        raise exceptions.MalformedSchemaError(
            "Referenced object does not have any properties.")
    logical_name = "id"
    id_spec = properties.get(logical_name)
    if id_spec is None:
        raise exceptions.MalformedSchemaError(
            "Referenced object does not have id property.")
    # Preparing specification
    prepared_id_spec = helpers.prepare_schema(schema=id_spec, schemas=schemas)
    id_type = prepared_id_spec.get("type")
    if id_type is None:
        raise exceptions.MalformedSchemaError(
            "Referenced object id property does not have a type.")

    return {"type": id_type, "x-foreign-key": f"{tablename}.id"}
Example #5
0
def test_miss():
    """
    GIVEN empty source
    WHEN get_ext_prop is called with the source
    THEN None is returned.
    """
    assert helpers.get_ext_prop(source={}, name="missing") is None
Example #6
0
def test_valid(name, value):
    """
    GIVEN property and valid value
    WHEN get_ext_prop is called with a source made of the property and value
    THEN the value is returned.
    """
    source = {name: value}

    returned_value = helpers.get_ext_prop(source=source, name=name)

    assert returned_value == value
def test_miss_default():
    """
    GIVEN empty source and default value
    WHEN get_ext_prop is called with the source and default value
    THEN default value is returned.
    """
    default = "value 1"

    value = helpers.get_ext_prop(source={}, name="missing", default=default)

    assert value == default
def test_unique_constraint_valid(value):
    """
    GIVEN value for x-composite-unique that has a valid format
    WHEN get_ext_prop with x-composite-unique and the value
    THEN the value is returned.
    """
    name = "x-composite-unique"
    source = {name: value}

    returned_value = helpers.get_ext_prop(source=source, name=name)

    assert returned_value == value
def test_composite_index_valid(value):
    """
    GIVEN value for x-composite-index that has a valid format
    WHEN get_ext_prop with x-composite-index and the value
    THEN the value is returned.
    """
    name = "x-composite-index"
    source = {name: value}

    returned_value = helpers.get_ext_prop(source=source, name=name)

    assert returned_value == value
Example #10
0
def _spec_to_column(*,
                    spec: types.Schema,
                    required: typing.Optional[bool] = None):
    """
    Convert specification to a SQLAlchemy column.

    Args:
        spec: The schema for the column.
        required: Whether the object property is required.

    Returns:
        The SQLAlchemy column based on the schema.

    """
    # Keep track of column arguments
    args: typing.Tuple[typing.Any, ...] = ()
    kwargs: types.Schema = {}

    # Calculate column modifiers
    kwargs["nullable"] = _calculate_nullable(spec=spec, required=required)
    if helpers.get_ext_prop(source=spec, name="x-primary-key"):
        kwargs["primary_key"] = True
    autoincrement = helpers.get_ext_prop(source=spec, name="x-autoincrement")
    if autoincrement is not None:
        if autoincrement:
            kwargs["autoincrement"] = True
        else:
            kwargs["autoincrement"] = False
    if helpers.get_ext_prop(source=spec, name="x-index"):
        kwargs["index"] = True
    if helpers.get_ext_prop(source=spec, name="x-unique"):
        kwargs["unique"] = True
    foreign_key = helpers.get_ext_prop(source=spec, name="x-foreign-key")
    if foreign_key:
        args = (*args, sqlalchemy.ForeignKey(foreign_key))

    # Calculating type of column
    type_ = _determine_type(spec=spec)

    return sqlalchemy.Column(type_, *args, **kwargs)
def test_pop():
    """
    GIVEN property and valid value
    WHEN get_ext_property is called with the name, value and pop set
    THEN the key is removed from the dictionary.
    """
    name = "x-dict-ignore"
    value = True
    source = {name: value}

    returned_value = helpers.get_ext_prop(source=source, name=name, pop=True)

    assert returned_value == value
    assert source == {}
Example #12
0
def handle_object_reference(*, spec: types.Schema, schemas: types.Schemas,
                            fk_column: str) -> types.Schema:
    """
    Determine the foreign key schema for an object reference.

    Args:
        spec: The schema of the object reference.
        schemas: All defined schemas.
        fk_column: The foreign column name to use.

    Returns:
        The foreign key schema.

    """
    tablename = helpers.get_ext_prop(source=spec, name="x-tablename")
    if not tablename:
        raise exceptions.MalformedSchemaError(
            "Referenced object is missing x-tablename property.")
    properties = spec.get("properties")
    if properties is None:
        raise exceptions.MalformedSchemaError(
            "Referenced object does not have any properties.")
    fk_logical_name = fk_column if fk_column is not None else "id"
    fk_spec = properties.get(fk_logical_name)
    if fk_spec is None:
        raise exceptions.MalformedSchemaError(
            f"Referenced object does not have {fk_logical_name} property.")
    # Preparing specification
    prepared_fk_spec = helpers.prepare_schema(schema=fk_spec, schemas=schemas)
    fk_type = prepared_fk_spec.get("type")
    if fk_type is None:
        raise exceptions.MalformedSchemaError(
            f"Referenced object {fk_logical_name} property does not have a type."
        )

    return {"type": fk_type, "x-foreign-key": f"{tablename}.{fk_logical_name}"}
Example #13
0
def _many_to_many_column_artifacts(
        *, model_schema: types.Schema,
        schemas: types.Schemas) -> _ManyToManyColumnArtifacts:
    """
    Retrieve column artifacts of a secondary table for a many to many relationship.

    Args:
        model_schema: The schema for one side of the many to many relationship.
        schemas: Used to resolve any $ref.

    Returns:
        The artifacts needed to construct a column of the secondary table in a many to
        many relationship.

    """
    # Resolve $ref and merge allOf
    model_schema = helpers.prepare_schema(schema=model_schema, schemas=schemas)

    # Check schema type
    model_type = model_schema.get("type")
    if model_type is None:
        raise exceptions.MalformedSchemaError("Every schema must have a type.")
    if model_type != "object":
        raise exceptions.MalformedSchemaError(
            "A schema that is part of a many to many relationship must be of type "
            "object.")

    # Retrieve table name
    tablename = helpers.get_ext_prop(source=model_schema, name="x-tablename")
    if tablename is None:
        raise exceptions.MalformedSchemaError(
            "A schema that is part of a many to many relationship must set the "
            "x-tablename property.")

    # Find primary key
    properties = model_schema.get("properties")
    if properties is None:
        raise exceptions.MalformedSchemaError(
            "A schema that is part of a many to many relationship must have properties."
        )
    if not properties:
        raise exceptions.MalformedSchemaError(
            "A schema that is part of a many to many relationship must have at least 1 "
            "property.")
    type_ = None
    format_ = None
    for property_name, property_schema in properties.items():
        if helpers.peek.primary_key(schema=property_schema, schemas=schemas):
            if type_ is not None:
                raise exceptions.MalformedSchemaError(
                    "A schema that is part of a many to many relationship must have "
                    "exactly 1 primary key.")
            try:
                type_ = helpers.peek.type_(schema=property_schema,
                                           schemas=schemas)
            except exceptions.TypeMissingError:
                raise exceptions.MalformedSchemaError(
                    "A schema that is part of a many to many relationship must define "
                    "a type for the primary key.")
            format_ = helpers.peek.format_(schema=property_schema,
                                           schemas=schemas)
            max_length = helpers.peek.max_length(schema=property_schema,
                                                 schemas=schemas)
            column_name = property_name
    if type_ is None:
        raise exceptions.MalformedSchemaError(
            "A schema that is part of a many to many relationship must have "
            "exactly 1 primary key.")
    if type_ in {"object", "array"}:
        raise exceptions.MalformedSchemaError(
            "A schema that is part of a many to many relationship cannot define it's "
            "primary key to be of type object nor array.")

    return _ManyToManyColumnArtifacts(type_, format_, tablename, column_name,
                                      max_length)
Example #14
0
def handle_array(
    *,
    spec: types.Schema,
    model_schema: types.Schema,
    schemas: types.Schemas,
    logical_name: str,
) -> typing.Tuple[typing.List[typing.Tuple[str, typing.Type]], types.Schema]:
    """
    Generate properties for a reference to another object through an array.

    Assume that when any allOf and $ref are resolved in the root spec the type is
    array.

    Args:
        spec: The schema for the column.
        model_schema: The schema of the one to many parent.
        schemas: Used to resolve any $ref.
        logical_name: The logical name in the specification for the schema.

    Returns:
        The logical name and the relationship for the referenced object.

    """
    # Resolve any allOf and $ref
    spec = helpers.prepare_schema(schema=spec, schemas=schemas)

    # Get item specification
    item_spec = spec.get("items")
    if item_spec is None:
        raise exceptions.MalformedRelationshipError(
            "An array property must include items property.")
    obj_artifacts = object_ref.gather_object_artifacts(
        spec=item_spec, logical_name=logical_name, schemas=schemas)

    # Check for uselist
    if obj_artifacts.uselist is not None:
        raise exceptions.MalformedRelationshipError(
            "x-uselist is not supported for one to many relationships.")

    # Check referenced specification
    ref_spec = helpers.prepare_schema(schema=obj_artifacts.spec,
                                      schemas=schemas)
    ref_type = ref_spec.get("type")
    if ref_type != "object":
        raise exceptions.MalformedRelationshipError(
            "One to many relationships must reference an object type schema.")
    ref_tablename = helpers.get_ext_prop(source=ref_spec, name="x-tablename")
    if ref_tablename is None:
        raise exceptions.MalformedRelationshipError(
            "One to many relationships must reference a schema with "
            "x-tablename defined.")

    # Construct relationship
    relationship_return = (
        logical_name,
        sqlalchemy.orm.relationship(
            obj_artifacts.ref_logical_name,
            backref=obj_artifacts.backref,
            secondary=obj_artifacts.secondary,
        ),
    )
    # Construct entry for the addition for the model schema
    spec_return = {
        "type": "array",
        "items": {
            "type": "object",
            "x-de-$ref": obj_artifacts.ref_logical_name
        },
    }
    # Add foreign key to referenced schema
    if obj_artifacts.secondary is None:
        _set_foreign_key(
            ref_model_name=obj_artifacts.ref_logical_name,
            model_schema=model_schema,
            schemas=schemas,
            fk_column=obj_artifacts.fk_column,
        )
    else:
        table = _construct_association_table(
            parent_schema=model_schema,
            child_schema=obj_artifacts.spec,
            schemas=schemas,
            tablename=obj_artifacts.secondary,
        )
        facades.models.set_association(table=table,
                                       name=obj_artifacts.secondary)

    return [relationship_return], spec_return
Example #15
0
def _set_foreign_key(
    *,
    ref_model_name: str,
    model_schema: types.Schema,
    schemas: types.Schemas,
    fk_column: str,
) -> None:
    """
    Set the foreign key on an existing model or add it to the schemas.

    Args:
        ref_model_name: The name of the referenced model.
        model_schema: The schema of the one to many parent.
        schemas: All the model schemas.
        fk_column: The name of the foreign key column.

    """
    # Check that model is in schemas
    if ref_model_name not in schemas:
        raise exceptions.MalformedRelationshipError(
            f"{ref_model_name} referenced in relationship was not found in the "
            "schemas.")

    # Calculate foreign key specification
    fk_spec = object_ref.handle_object_reference(spec=model_schema,
                                                 schemas=schemas,
                                                 fk_column=fk_column)

    # Calculate values for foreign key
    tablename = helpers.get_ext_prop(source=model_schema, name="x-tablename")
    fk_logical_name = f"{tablename}_{fk_column}"

    # Gather referenced schema
    ref_schema = schemas[ref_model_name]
    # Any top level $ref must already be resolved
    ref_schema = helpers.merge_all_of(schema=ref_schema, schemas=schemas)
    fk_required = object_ref.check_foreign_key_required(
        fk_spec=fk_spec,
        fk_logical_name=fk_logical_name,
        model_schema=ref_schema,
        schemas=schemas,
    )
    if not fk_required:
        return

    # Handle model already constructed
    ref_model: TOptUtilityBase = facades.models.get_model(name=ref_model_name)
    if ref_model is not None:
        # Construct foreign key
        _, fk_column = column.handle_column(schema=fk_spec)
        setattr(ref_model, fk_logical_name, fk_column)
        return

    # Handle model not constructed
    schemas[ref_model_name] = {
        "allOf": [
            schemas[ref_model_name],
            {
                "type": "object",
                "properties": {
                    fk_logical_name: {
                        **fk_spec, "x-dict-ignore": True
                    }
                },
            },
        ]
    }
Example #16
0
def _handle_object(
    *,
    spec: types.Schema,
    schemas: types.Schemas,
    required: typing.Optional[bool] = None,
    logical_name: str,
) -> typing.List[typing.Tuple[str, typing.Union[sqlalchemy.Column,
                                                typing.Type]]]:
    """
    Generate properties for a reference to another object.

    Assume that, when any $ref and allOf are resolved, the schema is an object.

    Args:
        spec: The schema for the column.
        schemas: Used to resolve any $ref.
        required: Whether the object property is required.
        logical_name: The logical name in the specification for the schema.

    Returns:
        The logical name and the SQLAlchemy column for the foreign key and the logical
        name and relationship for the reference to the object.

    """
    # Default backref
    backref = None

    # Checking for $ref and allOf
    ref = spec.get("$ref")
    all_of = spec.get("allOf")

    if ref is not None:
        # Handling $ref
        ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                     schema=spec,
                                                     schemas=schemas)
        backref = helpers.get_ext_prop(source=spec, name="x-backref")
    elif all_of is not None:
        # Checking for $ref and x-backref counts
        ref_count = 0
        backref_count = 0
        for sub_spec in all_of:
            if sub_spec.get("$ref") is not None:
                ref_count += 1
            if sub_spec.get("x-backref") is not None:
                backref_count += 1
        if ref_count != 1:
            raise exceptions.MalformedManyToOneRelationshipError(
                "Many to One relationships defined with allOf must have exactly one "
                "$ref in the allOf list.")
        if backref_count > 1:
            raise exceptions.MalformedManyToOneRelationshipError(
                "Many to One relationships may have at most 1 x-backref defined."
            )

        # Handling allOf
        for sub_spec in all_of:
            backref = helpers.get_ext_prop(source=sub_spec, name="x-backref")
            if sub_spec.get("$ref") is not None:
                ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                             schema=sub_spec,
                                                             schemas=schemas)
    else:
        raise exceptions.MalformedManyToOneRelationshipError(
            "Many to One relationships are defined using either $ref or allOf."
        )

    # Resolving allOf
    spec = helpers.merge_all_of(schema=spec, schemas=schemas)

    # Handling object
    foreign_key_spec = _handle_object_reference(spec=spec, schemas=schemas)
    return_value = _handle_column(logical_name=f"{logical_name}_id",
                                  spec=foreign_key_spec,
                                  required=required)

    # Creating relationship
    return_value.append((logical_name,
                         sqlalchemy.orm.relationship(ref_logical_name,
                                                     backref=backref)))
    return return_value
Example #17
0
def gather_object_artifacts(*, spec: types.Schema, logical_name: str,
                            schemas: types.Schemas) -> ObjectArtifacts:
    """
    Collect artifacts from a specification for constructing an object reference.

    Get the prepared specification, reference logical name, back reference and foreign
    key column name from a raw object specification.

    Raise MalformedRelationshipError if neither $ref nor $allOf is found.
    Raise MalformedRelationshipError if uselist is defined but backref is not.
    Raise MalformedRelationshipError if multiple $ref, x-backref, x-secondary,
    x-foreign-key-column or x-uselist are found.

    Args:
        spec: The schema for the column.
        schemas: Used to resolve any $ref.
        logical_name: The logical name in the specification for the schema.

    Returns:
        The prepared specification, reference logical name, back reference and foreign
        key column.

    """
    # Default backref
    backref = None
    # Default uselist
    uselist = None
    # Default secondary
    secondary = None
    # Initial foreign key column
    fk_column = None

    # Checking for $ref and allOf
    ref = spec.get("$ref")
    all_of = spec.get("allOf")

    if ref is not None:
        # Handle $ref
        ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                     schema=spec,
                                                     schemas=schemas)
    elif all_of is not None:
        # Checking for $ref, and x-backref and x-foreign-key-column counts
        _check_object_all_of(all_of_spec=all_of)

        # Handle allOf
        for sub_spec in all_of:
            backref = helpers.get_ext_prop(source=sub_spec,
                                           name="x-backref",
                                           default=backref)
            uselist = helpers.get_ext_prop(source=sub_spec,
                                           name="x-uselist",
                                           default=uselist)
            secondary = helpers.get_ext_prop(source=sub_spec,
                                             name="x-secondary",
                                             default=secondary)
            fk_column = helpers.get_ext_prop(source=sub_spec,
                                             name="x-foreign-key-column",
                                             default=fk_column)
            if sub_spec.get("$ref") is not None:
                ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                             schema=sub_spec,
                                                             schemas=schemas)
    else:
        raise exceptions.MalformedRelationshipError(
            "Relationships are defined using either $ref or allOf.")

    # Resolving allOf
    spec = helpers.merge_all_of(schema=spec, schemas=schemas)

    # If backref has not been found look in referenced schema
    if backref is None:
        backref = helpers.get_ext_prop(source=spec, name="x-backref")
    # If uselist has not been found look in referenced schema
    if uselist is None:
        uselist = helpers.get_ext_prop(source=spec, name="x-uselist")
    # If secondary has not been found look in referenced schema
    if secondary is None:
        secondary = helpers.get_ext_prop(source=spec, name="x-secondary")
    # If foreign key column has not been found look in referenced schema
    if fk_column is None:
        fk_column = helpers.get_ext_prop(source=spec,
                                         name="x-foreign-key-column")
    # If foreign key column is still None, default to id
    if fk_column is None:
        fk_column = "id"

    # Check if uselist is defined and backref is not
    if uselist is not None and backref is None:
        raise exceptions.MalformedRelationshipError(
            "Relationships with x-uselist defined must also define x-backref.")

    return ObjectArtifacts(spec, ref_logical_name, backref, fk_column, uselist,
                           secondary)
Example #18
0
def check_foreign_key_required(
    *,
    fk_spec: types.Schema,
    fk_logical_name: str,
    model_schema: types.Schema,
    schemas: types.Schemas,
) -> bool:
    """
    Check whether a foreign key has already been defined.

    Assume model_schema has already resolved any $ref and allOf at the object level.
    They may not have been resolved at the property level.

    Check whether the proposed logical name is already defined on the model schema. If
    it has been, check that the type is correct and that the foreign key reference has
    been defined and points to the correct column.

    Raise MalformedRelationshipError if a property has already been defined with the
    same name as is proposed for the foreign key but it has the wrong type or does not
    define the correct foreign key constraint.

    Args:
        fk_spec: The proposed foreign key specification.
        fk_logical_name: The proposed name for the foreign key property.
        model_schema: The schema for the model on which the foreign key is proposed to
            be added.
        schemas: Used to resolve any $ref at the property level.

    Returns:
        Whether defining the foreign key is necessary given the model schema.

    """
    properties = model_schema["properties"]
    model_fk_spec = properties.get(fk_logical_name)
    if model_fk_spec is None:
        return True
    model_fk_spec = helpers.prepare_schema(schema=model_fk_spec,
                                           schemas=schemas)

    # Check type
    model_fk_type = model_fk_spec.get("type")
    if model_fk_type is None:
        raise exceptions.MalformedRelationshipError(
            f"{fk_logical_name} does not have a type. ")
    fk_type = fk_spec["type"]
    if model_fk_type != fk_type:
        raise exceptions.MalformedRelationshipError(
            "The foreign key required for the relationship has a different type than "
            "the property already defined under that name. "
            f"The required type is {fk_type}. "
            f"The {fk_logical_name} property has the {model_fk_type} type.")

    # Check foreign key constraint
    model_foreign_key = helpers.get_ext_prop(source=model_fk_spec,
                                             name="x-foreign-key")
    foreign_key = fk_spec["x-foreign-key"]
    if model_foreign_key is None:
        raise exceptions.MalformedRelationshipError(
            f"The property already defined under {fk_logical_name} does not define a "
            'foreign key constraint. Use the "x-foreign-key" extension property to '
            f'define a foreign key constraint, for example: "{foreign_key}".')
    if model_foreign_key != foreign_key:
        raise exceptions.MalformedRelationshipError(
            "The foreign key required for the relationship has a different foreign "
            "key constraint than the property already defined under that name. "
            f"The required constraint is {foreign_key}. "
            f"The {fk_logical_name} property has the {model_foreign_key} constraint."
        )

    return False