Skip to content

Commit

Permalink
Merge pull request #14 from wayfair-incubator/jp_custom_resources
Browse files Browse the repository at this point in the history
Support Custom Resources
  • Loading branch information
jhpierce authored Apr 1, 2022
2 parents d390179 + e2e1dee commit e39db2a
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 122 deletions.
4 changes: 2 additions & 2 deletions gator/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ class InvalidSpecificationError(GatorError):
"""There was an error with retrieving specification data."""


class FilterExecutionError(GatorError):
"""There was an error executing logic for a FilterResource."""
class InvalidResourceError(GatorError):
"""The given custom resource definition was not valid."""
113 changes: 92 additions & 21 deletions gator/resources/build.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,61 @@
from typing import Dict, Type
from typing import Dict, List, Optional, Type

import yaml
from pydantic import ValidationError, parse_obj_as
from pydantic import ValidationError

from gator.exceptions import InvalidSpecificationError
from gator.resources.changeset import Changeset
from gator.exceptions import InvalidResourceError, InvalidSpecificationError
from gator.resources.code_changes import (
NewFileCodeChangeV1Alpha,
RegexReplaceCodeChangeV1Alpha,
RemoveFileCodeChangeV1Alpha,
)
from gator.resources.filters.regex_filter import RegexFilterV1Alpha
from gator.resources.models import GatorResource
from gator.resources.models import (
BaseModelForbidExtra,
CodeChangeResource,
FilterResource,
GatorResource,
)

ACTIVE_RESOURCES: Dict[str, Type[GatorResource]] = {
ACTIVE_RESOURCES = {
"NewFileCodeChange": NewFileCodeChangeV1Alpha,
"RemoveFileCodeChange": RemoveFileCodeChangeV1Alpha,
"RegexReplaceCodeChange": RegexReplaceCodeChangeV1Alpha,
"RegexFilter": RegexFilterV1Alpha,
}


def build_gator_resource(resource_dict: Dict) -> GatorResource:
if "kind" not in resource_dict:
raise InvalidSpecificationError(
"Resources must have a 'kind' field at the top level"
)
class _ResourceWithValidation(GatorResource):
"""Define a subclass of Gator Resource that tricks"""

_class = ACTIVE_RESOURCES.get(resource_dict["kind"])
@classmethod
def __get_validators__(cls):
yield cls.return_kind

if not _class:
raise InvalidSpecificationError(
f"Resource kind: {resource_dict['kind']} is not active"
)
@classmethod
def return_kind(cls, values):
try:
kind = values["kind"]
except KeyError:
raise ValueError(f"Missing required 'kind' field for kind: {values}")
try:
return ACTIVE_RESOURCES[kind].parse_obj(values)
except KeyError:
raise ValueError(f"Incorrect kind: {kind}")

try:
return _class.parse_obj(resource_dict)
except ValidationError as e:
raise InvalidSpecificationError from e

class ChangesetSpecV1AlphaSpec(BaseModelForbidExtra):
name: str
issue_title: Optional[str]
issue_body: Optional[str]
filters: Optional[List[_ResourceWithValidation]]
code_changes: Optional[List[_ResourceWithValidation]]


class Changeset(BaseModelForbidExtra):
kind = "Changeset"
version = "v1alpha"
spec: ChangesetSpecV1AlphaSpec


def build_changeset(spec: str) -> Changeset:
Expand All @@ -54,6 +72,59 @@ def build_changeset(spec: str) -> Changeset:
raise InvalidSpecificationError from e

try:
return parse_obj_as(Changeset, changeset_dict)
return Changeset.parse_obj(changeset_dict)
except ValidationError as e:
raise InvalidSpecificationError from e


def build_gator_resource(resource_dict: Dict) -> GatorResource:
"""
Build a Gator Resource Pydantic model from a dictionary representation.
:param resource_dict: dictionary representation of the resource to build a model for.
:raises InvalidSpecificationError: If the resource dict could not be built into a model.
:return: Fully-constructed and validated GatorResource.
"""
kind = resource_dict.get("kind")
if not kind:
raise InvalidSpecificationError(
"Resource is not valid, must contain top-level field 'kind'"
)

resource_class = ACTIVE_RESOURCES.get(kind)
if not resource_class:
raise InvalidSpecificationError(f"No active resources found of kind: {kind}")

try:
return resource_class.parse_obj(resource_dict)
except ValidationError as e:
raise InvalidSpecificationError(
"Could not parse resource spec into the corresponding model"
) from e


def register_custom_resource(resource_class: Type) -> None:
"""
Register a custom Gator resource.
Use this function to register a custom resource with Gator. This
:param resource_class: Pydantic class, extending CodeChangeResource or FilterResource,
that contains the business logic for executing the resource
"""
global ACTIVE_RESOURCES

if not issubclass(resource_class, CodeChangeResource) or issubclass(
resource_class, FilterResource
):
raise InvalidResourceError(
"Resource must subclass either CodeChangeResource or FilterResource"
)

try:
ACTIVE_RESOURCES[
resource_class.schema()["properties"]["kind"]["const"]
] = resource_class
except KeyError:
raise InvalidResourceError(
"Custom resource must define a class variable 'kind'"
)
30 changes: 0 additions & 30 deletions gator/resources/changeset.py

This file was deleted.

8 changes: 2 additions & 6 deletions gator/resources/code_changes/new_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
from typing import List

from gator.constants import VERSION_V1_ALPHA
from gator.resources.models import (
BaseModelForbidExtra,
CodeChangeResource,
GatorResourceSpec,
)
from gator.resources.models import BaseModelForbidExtra, CodeChangeResource

_logger = logging.getLogger(__name__)

Expand All @@ -18,7 +14,7 @@ class FileDetails(BaseModelForbidExtra):
file_content: str


class NewFileCodeChangeV1AlphaSpec(GatorResourceSpec):
class NewFileCodeChangeV1AlphaSpec(BaseModelForbidExtra):
files: List[FileDetails]


Expand Down
8 changes: 2 additions & 6 deletions gator/resources/code_changes/regex_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
from typing import List, Pattern

from gator.constants import VERSION_V1_ALPHA
from gator.resources.models import (
BaseModelForbidExtra,
CodeChangeResource,
GatorResourceSpec,
)
from gator.resources.models import BaseModelForbidExtra, CodeChangeResource
from gator.resources.util import get_recursive_path_contents

_logger = logging.getLogger(__name__)
Expand All @@ -20,7 +16,7 @@ class RegexReplacementDetails(BaseModelForbidExtra):
replace_term: str


class RegexReplaceCodeChangeV1AlphaSpec(GatorResourceSpec):
class RegexReplaceCodeChangeV1AlphaSpec(BaseModelForbidExtra):
replacement_details: List[RegexReplacementDetails]


Expand Down
4 changes: 2 additions & 2 deletions gator/resources/code_changes/remove_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from typing import List

from gator.constants import VERSION_V1_ALPHA
from gator.resources.models import CodeChangeResource, GatorResourceSpec
from gator.resources.models import BaseModelForbidExtra, CodeChangeResource

_logger = logging.getLogger(__name__)


class RemoveFileCodeChangeV1AlphaSpec(GatorResourceSpec):
class RemoveFileCodeChangeV1AlphaSpec(BaseModelForbidExtra):
files: List[str]


Expand Down
4 changes: 2 additions & 2 deletions gator/resources/filters/regex_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from typing import List

from gator.constants import DEFAULT_REGEX_MODES
from gator.resources.models import FilterResource, GatorResourceSpec
from gator.resources.models import BaseModelForbidExtra, FilterResource
from gator.resources.util import get_recursive_path_contents

_logger = logging.getLogger(__name__)


class RegexFilterV1AlphaSpec(GatorResourceSpec):
class RegexFilterV1AlphaSpec(BaseModelForbidExtra):
regex: str
paths: List[str]

Expand Down
53 changes: 5 additions & 48 deletions gator/resources/models.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
from abc import abstractmethod
from enum import Enum
from pathlib import Path
from typing import Any, Dict

from pydantic import BaseModel

from gator.constants import RESOURCE_KIND_GENERIC, RESOURCE_VERSION_UNUSABLE


class GatorResourceType(Enum):
FILTER = "filter"
CODE_CHANGE = "code_change"
CHANGESET = "changeset"


class BaseModelForbidExtra(BaseModel):
"""
Expand All @@ -23,56 +13,23 @@ class Config:
extra = "forbid"


class GatorResourceSpec(BaseModelForbidExtra):
"""
GatorResourceSpec's will define the actual configurable fields of a resource.
"""


class GatorResource(BaseModelForbidExtra):
class GatorResource(BaseModel):
"""
GatorResources define a specific `kind`, `version`, and associated spec fields.
Base class for all Gator Resources.
"""

spec: GatorResourceSpec
class Config:
fields = {"kind": dict(const=True)}
extra = "forbid"


class FilterResource(GatorResource):

kind = RESOURCE_KIND_GENERIC
version = RESOURCE_VERSION_UNUSABLE

@abstractmethod
def matches(self, repo_path: Path) -> bool:
"""
Concrete FilterResources must implement the `matches` function.
:param repo_path: The path to the directory where repository content exists
"""
...


class CodeChangeResource(GatorResource):

kind = RESOURCE_KIND_GENERIC
version = RESOURCE_VERSION_UNUSABLE

@abstractmethod
def make_code_changes(self, repo_path: Path) -> None:
"""
Concrete CodeChangeResources must implement the `apply` function.
`make_code_changes` will apply a code change given the repository
:repo_path: The path to the directory where repository content exists
"""
...


class GatorResourceData(BaseModelForbidExtra):
"""
Represents shape of data provided to `build_gator_resource()`.
"""

kind: str
version: str
spec: Dict[str, Any]
41 changes: 37 additions & 4 deletions testpad.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from gator.resources.build import build_changeset
from gator.resources.build import build_changeset, register_custom_resource
from pydantic import BaseModel
from pathlib import Path
from gator.resources.models import CodeChangeResource

spec = """
kind: Changeset
version: v1alpha
stuff: "stuff"
spec:
name: time to do a thing
issue_title: stuff
issue_body: other stuff
filters:
Expand All @@ -13,7 +16,7 @@
spec:
regex: '([ \t]+)bo1c2\:'
paths:
- k8s.yaml
- "asd"
- k8s.yml
code_changes:
- kind: RegexReplaceCodeChange
Expand All @@ -26,4 +29,34 @@
- "requirements-test.txt"
"""

build_changeset(spec)
print(build_changeset(spec))

# class DoNothingSpec(BaseModel):
# some_value: str
#
# class DoNothingCodeChange(CodeChangeResource):
# kind = 'DoNothingCodeChange'
# version = 'v1alpha'
# spec: DoNothingSpec
#
# def make_code_changes(self, path: Path) -> None:
# pass
#
# register_custom_resource(DoNothingCodeChange)
#
# another_spec = """
# kind: Changeset
# version: v1alpha
# spec:
# name: time to do a thing
# issue_title: stuff
# issue_body: other stuff
# code_changes:
# - kind: DoNothingCodeChange
# version: v1alpha
# spec:
# some_value: "concrete value"
#
# """
#
# print(build_changeset(another_spec))
Loading

0 comments on commit e39db2a

Please sign in to comment.