-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #99 from epsylabs/feature/appsync
add support for appsync plugin
- Loading branch information
Showing
13 changed files
with
701 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
[bumpversion] | ||
current_version = 2.16.8 | ||
current_version = 2.17.0 | ||
commit = True | ||
tag = True | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[tool.poetry] | ||
name = "serverless-builder" | ||
version = "2.16.8" | ||
version = "2.17.0" | ||
description = "Python interface to easily generate `serverless.yml`." | ||
keywords = ["library", "serverless"] | ||
authors = ["Epsy <[email protected]>"] | ||
|
@@ -22,11 +22,14 @@ packages = [ | |
] | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.8" | ||
python = "^3.9" | ||
PyYAML = "^6.0" | ||
troposphere = "^4.0.2" | ||
inflection = "^0.5.1" | ||
awacs = "^2.1.0" | ||
strawberry-graphql = "^0.256.1" | ||
pydantic = {extras = ["email"], version = "^2.6.1"} | ||
pydantic-extra-types = "^2.5.0" | ||
|
||
[tool.poetry.dev-dependencies] | ||
bump2version = "^1.0.1" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import itertools | ||
|
||
from serverless.aws.functions.generic import Function | ||
from serverless.service.types import YamlOrderedDict | ||
from serverless.service.plugins.appsync import AppSync | ||
|
||
|
||
import importlib | ||
|
||
|
||
def import_variable(module_name: str, variable_name: str): | ||
module = importlib.import_module(module_name) | ||
if not hasattr(module, variable_name): | ||
return None | ||
|
||
return getattr(module, variable_name) | ||
|
||
|
||
class AppSyncFunction(Function): | ||
yaml_tag = "!AppSyncFunction" | ||
|
||
def __init__(self, service, name, description, handler=None, timeout=None, layers=None, **kwargs): | ||
super().__init__(service, name, description, handler, timeout, layers, **kwargs) | ||
|
||
module_name, function_name = self.handler.rsplit(".", 1) | ||
graphql_app = import_variable(module_name, "app") | ||
|
||
if not graphql_app: | ||
return | ||
|
||
plugin = service.plugins.get(AppSync) | ||
|
||
plugin.dataSources[str(self.key.pascal)] = { | ||
"type": "AWS_LAMBDA", | ||
"config": {"functionName": str(self.key.pascal)}, | ||
} | ||
|
||
for resolver in graphql_app._resolver_registry.resolvers.keys(): | ||
plugin.resolvers[resolver] = {"kind": "UNIT", "dataSource": str(self.key.pascal)} | ||
|
||
@classmethod | ||
def to_yaml(cls, dumper, data): | ||
return super().to_yaml(dumper, data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .plugin import AppSync |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import inspect | ||
import typing | ||
import re | ||
from pathlib import Path | ||
from typing import get_type_hints, List, Type, get_args, get_origin | ||
from datetime import date, datetime | ||
|
||
import strawberry | ||
from pydantic import BaseModel | ||
from pydantic_extra_types.phone_numbers import PhoneNumber | ||
from strawberry.experimental.pydantic import input as strawberry_input | ||
from strawberry.experimental.pydantic import type as strawberry_type | ||
|
||
from serverless.service.plugins.appsync.resolver import ResolverManager, Resolver | ||
|
||
|
||
@strawberry.scalar | ||
class AWSPhone: | ||
@staticmethod | ||
def serialize(value: PhoneNumber) -> str: | ||
return str(value) | ||
|
||
@staticmethod | ||
def parse_value(value: str) -> PhoneNumber: | ||
return PhoneNumber(value) | ||
|
||
|
||
@strawberry.scalar | ||
class AWSDate: | ||
@staticmethod | ||
def serialize(value: date) -> str: | ||
return str(value) | ||
|
||
@staticmethod | ||
def parse_value(value: str) -> date: | ||
return date.fromisoformat(value) | ||
|
||
|
||
@strawberry.scalar | ||
class AWSDateTime: | ||
@staticmethod | ||
def serialize(value: datetime) -> str: | ||
return value.isoformat() | ||
|
||
@staticmethod | ||
def parse_value(value: str) -> datetime: | ||
return datetime.fromisoformat(value) | ||
|
||
|
||
class SchemaBuilder: | ||
def __init__(self, resolver): | ||
self.resolver = resolver | ||
self.models = {} | ||
self._types = {strawberry_type: {}, strawberry_input: {}} | ||
self.strawberry_types = {} | ||
|
||
def add_model(self, model): | ||
self.strawberry_types[self._extract_name(model)] = strawberry_type(model=model, all_fields=True)( | ||
type(model.__name__, (), {}) | ||
) | ||
self.models[self._extract_name(model)] = model | ||
|
||
return self | ||
|
||
def import_models(self, models_module): | ||
for model in self._get_pydantic_models(models_module): | ||
self.add_model(model) | ||
|
||
return self | ||
|
||
def render(self): | ||
manager = ResolverManager(self) | ||
|
||
for name, definition in self.resolver._resolver_registry.resolvers.items(): | ||
parameters, output = self._get_function_signature(definition["func"]) | ||
|
||
manager.register(Resolver(name, parameters, output)) | ||
|
||
content = str( | ||
strawberry.Schema( | ||
query=manager.query(), | ||
mutation=manager.mutations(), | ||
scalar_overrides={PhoneNumber: AWSPhone, date: AWSDate, datetime: AWSDateTime}, | ||
) | ||
) | ||
|
||
import __main__ as main | ||
|
||
content = re.sub(r"scalar AWS(DateTime|Phone|Date)\n+", "", content, 0, re.MULTILINE) | ||
|
||
with open(Path(main.__file__).stem + ".graphql", "w+") as f: | ||
f.write(content) | ||
|
||
def as_output(self, pydantic_type: type[BaseModel]): | ||
return self.as_type(pydantic_type, strawberry_type) | ||
|
||
def as_input(self, pydantic_type: type[BaseModel]): | ||
return self.as_type(pydantic_type, strawberry_input) | ||
|
||
def as_type(self, pydantic_type: type[BaseModel], output_type): | ||
resolver_type = pydantic_type | ||
|
||
if get_origin(pydantic_type) is list: | ||
resolver_type = get_args(pydantic_type)[0] | ||
|
||
if issubclass(resolver_type, BaseModel): | ||
if resolver_type.__name__ in self._types[output_type]: | ||
return self._types[output_type][resolver_type.__name__] | ||
|
||
resolved = output_type(model=resolver_type, all_fields=True)(type(resolver_type.__name__, (), {})) | ||
|
||
self._types[output_type][resolver_type.__name__] = resolved | ||
else: | ||
resolved = resolver_type | ||
|
||
if get_origin(pydantic_type) is list: | ||
return typing.List[resolved] | ||
|
||
return resolved | ||
|
||
def _extract_name(self, model): | ||
return model.__name__.split(".")[-1] | ||
|
||
def _get_pydantic_models(self, module) -> List[Type[BaseModel]]: | ||
return [ | ||
obj | ||
for name, obj in inspect.getmembers(module) | ||
if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj.__module__ == module.__name__ | ||
] | ||
|
||
def _get_function_signature(self, func): | ||
signature = inspect.signature(func) | ||
|
||
type_hints = get_type_hints(func) | ||
|
||
parameters = {param_name: type_hints.get(param_name, "No type hint") for param_name in signature.parameters} | ||
|
||
return_type = type_hints.get("return", "No return type hint") | ||
|
||
return parameters, return_type |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from serverless.service.plugins.generic import Generic | ||
from serverless.service.types import YamlOrderedDict | ||
|
||
|
||
class IAMAuthentication(YamlOrderedDict): | ||
yaml_tag = "!IAMAuthentication" | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self.type = "AWS_IAM" | ||
|
||
|
||
class AppSync(Generic): | ||
""" | ||
Plugin: https://github.com/sid88in/serverless-appsync-plugin | ||
""" | ||
|
||
yaml_tag = "!AppSyncPlugin" | ||
|
||
def __init__( | ||
self, schema="schema.graphql", authentication=None, xray=False, resolvers=None, data_sources=None, **kwargs | ||
): | ||
super().__init__("serverless-appsync-plugin") | ||
self.schema = schema | ||
self.authentication = authentication or IAMAuthentication() | ||
self.xrayEnabled = xray | ||
self.dataSources = data_sources or {} | ||
self.resolvers = resolvers or {} | ||
self.update(kwargs) | ||
|
||
def enable(self, service): | ||
export = dict(self) | ||
export["name"] = str(service.service) | ||
|
||
service.appSync = export |
Oops, something went wrong.