Skip to content

Commit

Permalink
Merge pull request #30 from dbluhm/feat/infer-material
Browse files Browse the repository at this point in the history
feat: infer material based on known material properties
  • Loading branch information
dbluhm authored May 5, 2021
2 parents 4a10535 + 823911f commit 26e75a3
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 123 deletions.
13 changes: 5 additions & 8 deletions pydid/doc/builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""DID Document and resource builders."""

from typing import Any, Iterator, List, Optional, Type, Union
from typing import Iterator, List, Optional, Type, Union

from ..did import DID
from ..did_url import DIDUrl
Expand Down Expand Up @@ -39,17 +39,14 @@ def _default_id_generator(self):
def add(
self,
type_: Type[VerificationMethod],
material: Any,
ident: Optional[str] = None,
controller: DID = None,
**kwargs
):
"""Add verification method from parts and context."""
ident = ident or next(self._id_generator)
controller = controller or self._did
vmethod = type_.make(
id_=self._did.ref(ident), controller=controller, material=material, **kwargs
)
vmethod = type_.make(id=self._did.ref(ident), controller=controller, **kwargs)
self.methods.append(vmethod)
return vmethod

Expand Down Expand Up @@ -174,14 +171,14 @@ class DIDDocumentBuilder:

def __init__(
self,
id_: Union[str, DID],
id: Union[str, DID],
context: List[str] = None,
*,
also_known_as: List[str] = None,
controller: Union[List[str], List[DID]] = None
):
"""Initliaze builder."""
self.id: DID = DID(id_)
self.id: DID = DID(id)
self.context = context or self.DEFAULT_CONTEXT
self.also_known_as = also_known_as
self.controller = controller
Expand All @@ -202,7 +199,7 @@ def __init__(
def from_doc(cls, doc: DIDDocument) -> "DIDDocumentBuilder":
"""Create a Builder from an existing DIDDocument."""
builder = cls(
id_=doc.id,
id=doc.id,
context=doc.context,
also_known_as=doc.also_known_as,
controller=doc.controller,
Expand Down
33 changes: 26 additions & 7 deletions pydid/validation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Validation tools and helpers."""

from contextlib import contextmanager
from typing import Any, Callable, List, Type
from typing import Any, Callable, List, Set, Type

from pydantic import ValidationError, root_validator, create_model

Expand All @@ -17,20 +17,39 @@ def wrap_validation_error(error_to_raise: Type[Exception], message: str = None):
) from error


def coerce(transformers: List[Callable]):
"""Apply transformations to data before parsing into model."""
def validator(transformers: List[Callable]):
"""Transform or validate data before parsing into model."""

def _do_coercion(_model: Type, values: dict):
def _do_validation(_model: Type, values: dict):
for transformer in transformers:
values = transformer(values)
return values

def _coerce(typ: Type[Any]):
def _validation(typ: Type[Any]):
return create_model(
typ.__name__,
__module__=typ.__module__,
__base__=typ,
do_coercion=root_validator(pre=True, allow_reuse=True)(_do_coercion),
do_coercion=root_validator(pre=True, allow_reuse=True)(_do_validation),
)

return _coerce
return _validation


coerce = validator


def required_group(props: Set[str]):
"""Require at least one of the properties to be present."""

def _require_group(_model, values: dict):
defined_props = props & {
key for key, value in values.items() if value is not None
}
if len(defined_props) < 1:
raise ValueError(
"At least one of {} was required; none found".format(props)
)
return values

return root_validator(allow_reuse=True)(_require_group)
157 changes: 69 additions & 88 deletions pydid/verification_method.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,18 @@
"""DID Doc Verification Method."""

from typing import Any, Optional, Type, Union
from pydid.validation import required_group
from typing import ClassVar, Optional, Set, Type, Union

from inflection import underscore
from pydantic import create_model
from pydantic.class_validators import root_validator, validator
from typing_extensions import Annotated, Literal
import typing_extensions
from typing_extensions import Literal

from .did import DID
from .did_url import DIDUrl, InvalidDIDUrlError
from .resource import Resource


if hasattr(typing_extensions, "get_args"):

def annotated_args(annotated):
"""Return annotated arguments."""
return typing_extensions.get_args(annotated)

def is_annotated(type_):
"""Return if type is annotated."""
return typing_extensions.get_origin(type_) is Annotated

def get_type_hints(type_, **kwargs):
"""Return type hints for type."""
return typing_extensions.get_type_hints(type_, **kwargs)


else:

def annotated_args(annotated):
"""Return annotated arguments."""
return [annotated.__args__[0], *annotated.__args__[1]]

def is_annotated(type_):
"""Return if type is annotated."""
return hasattr(type_, "__origin__") and type_.__origin__ is Annotated

def get_type_hints(type_, **kwargs):
"""Return type hints for type."""
if type_ is VerificationMethod:
return {}
return {**type_.__annotations__, **get_type_hints(type_.__base__)}


class VerificationMaterial:
"""Type annotation marker for the material attribute of a verification method."""

Expand All @@ -56,14 +24,30 @@ class VerificationMaterialUnknown(NotImplementedError):
class VerificationMethod(Resource):
"""Representation of DID Document Verification Methods."""

material_properties: ClassVar[Set[str]] = {
"blockchain_account_id",
"ethereum_address",
"public_key_base58",
"public_key_gpg",
"public_key_hex",
"public_key_jwk",
"public_key_pem",
}

id: DIDUrl
type: str
controller: DID
_material_prop: Optional[str]
public_key_hex: Optional[str] = None
public_key_base58: Optional[str] = None
public_key_pem: Optional[str] = None
blockchain_account_id: Optional[str] = None
ethereum_address: Optional[str] = None
public_key_jwk: Optional[dict] = None
_material_prop: Optional[str] = None

def __init__(self, **data):
super().__init__(**data)
self._material_prop = self._determine_material_prop()
self._material_prop = self._material_prop or self._infer_material_prop()

@classmethod
def suite(cls: Type, typ: str, material: str, material_type: Type):
Expand All @@ -80,7 +64,6 @@ def suite(cls: Type, typ: str, material: str, material_type: Type):
lambda self, value: setattr(self, underscore(material), value),
)
model._material_prop = underscore(material)
model._determine_material_prop = classmethod(lambda cls: underscore(material))
return model

@validator("type", pre=True)
Expand Down Expand Up @@ -131,16 +114,34 @@ def _method_appears_to_contain_material(cls, values: dict):
"""Validate that the method appears to contain verification material."""
if len(values) < 4:
raise ValueError(
"Key material expected, only found id, type, and controller"
"Key material expected, found: {}".format(list(values.keys()))
)
return values

@root_validator
@classmethod
def _determine_material_prop(cls) -> Optional[str]:
"""Return the name of the property containing the verification material."""
for name, type_ in get_type_hints(cls, include_extras=True).items():
if is_annotated(type_) and VerificationMaterial in annotated_args(type_):
return name
def _no_more_than_one_material_prop(cls, values: dict):
"""Validate that exactly one material property was specified on method."""
set_material_properties = cls.material_properties & {
key for key, value in values.items() if value is not None
}
if len(set_material_properties) > 1:
raise ValueError(
"Found properties {}; only one is allowed".format(
", ".join(set_material_properties)
)
)
return values

def _infer_material_prop(self) -> Optional[str]:
"""
Guess the property that appears to be the verification material based
on known material property names.
"""

for prop, value in self:
if prop in self.material_properties and value is not None:
return prop

return None

Expand All @@ -149,7 +150,7 @@ def material(self):
"""Return material."""
if not self._material_prop:
raise VerificationMaterialUnknown(
"Verification Material was not specified on class"
"Verification Material is not known for this method"
)
return getattr(self, self._material_prop)

Expand All @@ -158,110 +159,90 @@ def material(self, value):
"""Set material."""
if not self._material_prop:
raise VerificationMaterialUnknown(
"Verification Material was not specified on class"
"Verification Material is not known for this method"
)
return setattr(self, self._material_prop, value)

@classmethod
def make(cls, id_: DIDUrl, controller: DID, material: Any, **kwargs):
"""Construct an instance of VerificationMethod, filling in known values."""
material_prop = cls._determine_material_prop()
if not material_prop:
raise VerificationMaterialUnknown(
"Verification Material was not specified on class"
)

return super(VerificationMethod, cls).make(
id=id_, controller=controller, **{material_prop: material}, **kwargs
)


# Verification Method Suites registered in DID Spec


class Base58VerificationMethod(VerificationMethod):
"""Verification Method where material is base58."""

public_key_base58: Annotated[str, VerificationMaterial]


class PemVerificationMethod(VerificationMethod):
"""Verification Method where material is pem."""

public_key_pem: Annotated[str, VerificationMaterial]


class JwkVerificationMethod(VerificationMethod):
"""Verification Method where material is jwk."""

public_key_jwk: Annotated[dict, VerificationMaterial]


class Ed25519VerificationKey2018(Base58VerificationMethod):
class Ed25519VerificationKey2018(VerificationMethod):
"""Ed25519VerificationKey2018 VerificationMethod."""

type: Literal["Ed25519VerificationKey2018"]
public_key_base58: str


class OpenPgpVerificationKey2019(PemVerificationMethod):
class OpenPgpVerificationKey2019(VerificationMethod):
"""OpenPgpVerificationKey2019 VerificationMethod."""

type: Literal["OpenPgpVerificationKey2019"]
public_key_pem: str


class JsonWebKey2020(JwkVerificationMethod):
class JsonWebKey2020(VerificationMethod):
"""JsonWebKey2020 VerificationMethod."""

type: Literal["JsonWebKey2020"]
public_key_jwk: dict


class EcdsaSecp256k1VerificationKey2019(JwkVerificationMethod):
class EcdsaSecp256k1VerificationKey2019(VerificationMethod):
"""EcdsaSecp256k1VerificationKey2019 VerificationMethod."""

type: Literal["EcdsaSecp256k1VerificationKey2019"]
_require_one_of = required_group({"public_key_jwk", "public_key_hex"})


class Bls1238G1Key2020(Base58VerificationMethod):
class Bls1238G1Key2020(VerificationMethod):
"""Bls1238G1Key2020 VerificationMethod."""

type: Literal["Bls1238G1Key2020"]
public_key_base58: str


class Bls1238G2Key2020(Base58VerificationMethod):
class Bls1238G2Key2020(VerificationMethod):
"""Bls1238G2Key2020 VerificationMethod."""

type: Literal["Bls1238G2Key2020"]
public_key_base58: str


class GpgVerifcationKey2020(VerificationMethod):
"""GpgVerifcationKey2020 VerificationMethod."""

type: Literal["GpgVerifcationKey2020"]
public_key_gpg: Annotated[str, VerificationMaterial]
public_key_gpg: str


class RsaVerificationKey2018(JwkVerificationMethod):
class RsaVerificationKey2018(VerificationMethod):
"""RsaVerificationKey2018 VerificationMethod."""

type: Literal["RsaVerificationKey2018"]
public_key_jwk: dict


class X25519KeyAgreementKey2019(Base58VerificationMethod):
class X25519KeyAgreementKey2019(VerificationMethod):
"""X25519KeyAgreementKey2019 VerificationMethod."""

type: Literal["X25519KeyAgreementKey2019"]
public_key_base58: str


class SchnorrSecp256k1VerificationKey2019(JwkVerificationMethod):
class SchnorrSecp256k1VerificationKey2019(VerificationMethod):
"""SchnorrSecp256k1VerificationKey2019 VerificationMethod."""

type: Literal["SchnorrSecp256k1VerificationKey2019"]


class EcdsaSecp256k1RecoveryMethod2020(JwkVerificationMethod):
class EcdsaSecp256k1RecoveryMethod2020(VerificationMethod):
"""EcdsaSecp256k1RecoveryMethod2020 VerificationMethod."""

type: Literal["EcdsaSecp256k1RecoveryMethod2020"]
_require_one_of = required_group(
{"public_key_jwk", "public_key_hex", "ethereum_address"}
)


class UnsupportedVerificationMethod(VerificationMethod):
Expand Down
Loading

0 comments on commit 26e75a3

Please sign in to comment.