Skip to content

Commit

Permalink
Merge pull request #99 from epsylabs/feature/appsync
Browse files Browse the repository at this point in the history
add support for appsync plugin
  • Loading branch information
mprzytulski authored Dec 31, 2024
2 parents ad1e428 + c6358bb commit 421bb53
Show file tree
Hide file tree
Showing 13 changed files with 701 additions and 132 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.8'
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
492 changes: 378 additions & 114 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions pyproject.toml
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]>"]
Expand All @@ -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"
Expand Down
11 changes: 2 additions & 9 deletions serverless/aws/features/stepfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,25 +322,18 @@ def __init__(
title=Identifier(self.name + "LogStream").resource,
LogGroupName=logs.resource.LogGroupName,
LogStreamName="dummy",
DependsOn=[
logs.resource.title
]
DependsOn=[logs.resource.title],
)
)

self.loggingConfig = dict(level="ERROR", includeExecutionData=True, destinations=[logs.get_att("Arn")])
self.dependsOn = [
logs.resource.title,
stream.title
]
self.dependsOn = [logs.resource.title, stream.title]
self.definition = Definition(description, auto_fallback, auto_catch)
self.events = events or []


def task(self, function=None, resource=None, name=None, end: Optional[bool] = None):
return self.definition.add(Task(function=function, resource=resource, name=name, end=end))


def map(self, name, steps, **kwargs):
return self.definition.add(Map(name, steps, **kwargs))

Expand Down
43 changes: 43 additions & 0 deletions serverless/aws/functions/appsync.py
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)
4 changes: 3 additions & 1 deletion serverless/aws/functions/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def __init__(
self.iamRoleStatementsName = self.iamRoleStatements.role
self.iamRoleStatementsInherit = True

configured = list(filter(lambda x: type(x) is dict and x.get("Ref") == "PythonRequirementsLambdaLayer", layers or []))
configured = list(
filter(lambda x: type(x) is dict and x.get("Ref") == "PythonRequirementsLambdaLayer", layers or [])
)
if (
self._service.plugins.get(PythonRequirements)
and self._service.plugins.get(PythonRequirements).layer
Expand Down
6 changes: 2 additions & 4 deletions serverless/aws/functions/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(
service,
name,
description,
events: List[WebsocketEvent]=None,
events: List[WebsocketEvent] = None,
handler=None,
timeout=None,
layers=None,
Expand All @@ -60,6 +60,4 @@ def __init__(
events = [WebsocketEvent("$default", None, None)]

for event in events:
self.trigger(
WebsocketEvent(event.route, event.routeResponseSelectionExpression, event.authorizer)
)
self.trigger(WebsocketEvent(event.route, event.routeResponseSelectionExpression, event.authorizer))
7 changes: 7 additions & 0 deletions serverless/aws/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from serverless.aws.functions.s3 import S3Function
from serverless.aws.functions.sqs import SQSFunction
from serverless.aws.functions.websocket import WebsocketFunction
from serverless.aws.functions.appsync import AppSyncFunction
from serverless.aws.iam import ServicePolicyBuilder
from serverless.service.environment import Environment
from serverless.service.types import Provider as BaseProvider
Expand Down Expand Up @@ -195,6 +196,12 @@ def websocket(self, name, description, events=None, **kwargs):

return fn

def appsync(self, name, description, **kwargs):
fn = AppSyncFunction(self.service, name, description, **kwargs)
self.service.functions.add(fn)

return fn


class Provider(BaseProvider, yaml.YAMLObject):
yaml_tag = "!Provider"
Expand Down
1 change: 1 addition & 0 deletions serverless/service/plugins/appsync/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .plugin import AppSync
140 changes: 140 additions & 0 deletions serverless/service/plugins/appsync/builder.py
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
35 changes: 35 additions & 0 deletions serverless/service/plugins/appsync/plugin.py
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
Loading

0 comments on commit 421bb53

Please sign in to comment.