Skip to content

Commit

Permalink
POC
Browse files Browse the repository at this point in the history
  • Loading branch information
TristenHarr committed Apr 20, 2024
1 parent 096e288 commit fc84b77
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
idea/
.idea/
__pycache__
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
### Proof of concept NDC lambda connector for Python.

This is a work in progress.

To see a more in-depth example that implements the Python SDK please see: https://github.com/hasura/ndc-turso-python

Currently, the proposed functionality will look something like this:

```python
from hasura_ndc_lambda import FunctionConnector, start

connector = FunctionConnector()


@connector.register_query
def do_the_thing(x: int) -> str:
print(x)
return "Hello World"


@connector.register_mutation
def some_mutation_function(arg1: str, arg2: int) -> str:
# Mutation function implementation
return f"Hey {arg1} {arg2}"


if __name__ == "__main__":
start(connector)
```

There will be support for built-in scalar types (int, float, str, bool) and also planned support for Pydantic types.


## TODO: Allow adding of async queries/mutations?

## TODO: Add Pydantic type introspections
1 change: 1 addition & 0 deletions http_requests/capabilities.http
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GET http://localhost:8080/capabilities
17 changes: 17 additions & 0 deletions http_requests/mutation.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
POST http://localhost:8080/mutation
Content-Type: application/json

{
"operations": [
{
"type": "procedure",
"name": "some_mutation_function",
"arguments": {
"arg1": "hello",
"arg2": 1
},
"fields": null
}
],
"collection_relationships": {}
}
72 changes: 72 additions & 0 deletions http_requests/query.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
POST http://localhost:8080/query
Content-Type: application/json

{
"collection": "Artist",
"query": {
"fields": {
"ArtistId": {
"type": "column",
"column": "ArtistId"
},
"Name": {
"type": "column",
"column": "Name"
},
"Albums": {
"type": "relationship",
"query": {
"fields": {
"AlbumId": {
"type": "column",
"column": "AlbumId"
},
"Title": {
"type": "column",
"column": "Title"
},
"Tracks": {
"type": "relationship",
"query": {
"fields": {
"TrackId": {
"type": "column",
"column": "TrackId"
},
"Name": {
"type": "column",
"column": "Name"
}
}
},
"relationship": "[{\"namespace\":\"unknown_namespace\",\"name\":\"Album\"},\"Tracks\"]",
"arguments": {}
}
}
},
"relationship": "[{\"namespace\":\"unknown_namespace\",\"name\":\"Artist\"},\"Albums\"]",
"arguments": {}
}
},
"limit": 10
},
"arguments": {},
"collection_relationships": {
"[{\"namespace\":\"unknown_namespace\",\"name\":\"Album\"},\"Tracks\"]": {
"column_mapping": {
"AlbumId": "AlbumId"
},
"relationship_type": "array",
"target_collection": "Track",
"arguments": {}
},
"[{\"namespace\":\"unknown_namespace\",\"name\":\"Artist\"},\"Albums\"]": {
"column_mapping": {
"ArtistId": "ArtistId"
},
"relationship_type": "array",
"target_collection": "Album",
"arguments": {}
}
}
}
1 change: 1 addition & 0 deletions http_requests/schema.http
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GET http://localhost:8080/schema
198 changes: 198 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
from hasura_ndc.connector import Connector
from hasura_ndc.main import start
from hasura_ndc.models import *
from pydantic import BaseModel
import inspect


class Configuration(BaseModel):
pass


class State(BaseModel):
pass


class FunctionConnector(Connector[Configuration, State]):

def __init__(self):
super().__init__(Configuration, State)
self.query_functions = {}
self.mutation_functions = {}

async def parse_configuration(self, configuration_dir: str) -> Configuration:
config = Configuration()
return config

async def try_init_state(self, configuration: Configuration, metrics: Any) -> State:
return State()

async def get_capabilities(self, configuration: Configuration) -> CapabilitiesResponse:
return CapabilitiesResponse(
version="^0.1.0",
capabilities=Capabilities(
query=QueryCapabilities(
aggregates=LeafCapability(),
variables=LeafCapability(),
explain=LeafCapability()
),
mutation=MutationCapabilities(
transactional=LeafCapability(),
explain=None
),
relationships=RelationshipCapabilities(
relation_comparisons=LeafCapability(),
order_by_aggregate=LeafCapability()
)
)
)

async def query_explain(self,
configuration: Configuration,
state: State,
request: QueryRequest) -> ExplainResponse:
pass

async def mutation_explain(self,
configuration: Configuration,
state: State,
request: MutationRequest) -> ExplainResponse:
pass

async def fetch_metrics(self,
configuration: Configuration,
state: State) -> Optional[None]:
pass

async def health_check(self,
configuration: Configuration,
state: State) -> Optional[None]:
pass

async def get_schema(self, configuration: Configuration) -> SchemaResponse:
functions = []
procedures = []

for name, func in self.query_functions.items():
function_info = self.generate_function_info(name, func)
functions.append(function_info)

for name, func in self.mutation_functions.items():
procedure_info = self.generate_procedure_info(name, func)
procedures.append(procedure_info)

schema_response = SchemaResponse(
scalar_types={},
functions=functions,
procedures=procedures,
object_types={},
collections=[]
)

return schema_response

def generate_function_info(self, name, func):
signature = inspect.signature(func)
arguments = {
arg_name: {
"type": self.get_type_info(arg_type)
}
for arg_name, arg_type in signature.parameters.items()
}
return FunctionInfo(
name=name,
arguments=arguments,
result_type=self.get_type_info(signature.return_annotation)
)

def generate_procedure_info(self, name, func):
signature = inspect.signature(func)
arguments = {
arg_name: {
"type": self.get_type_info(arg_type)
}
for arg_name, arg_type in signature.parameters.items()
}
return ProcedureInfo(
name=name,
arguments=arguments,
result_type=self.get_type_info(signature.return_annotation)
)

@staticmethod
def get_type_info(typ):
if isinstance(typ, inspect.Parameter):
typ = typ.annotation
if typ == int:
return Type(type="named", name="Int")
elif typ == float:
return Type(type="named", name="Float")
elif typ == str:
return Type(type="named", name="String")
elif typ == bool:
return Type(type="named", name="Boolean")
else:
return Type(type="named", name=typ.__name__)

async def query(self, configuration: Configuration, state: State, request: QueryRequest) -> QueryResponse:
print("QUERY")
print(request)
print(self.query_functions)
return [
RowSet(
aggregates=None,
rows=None
)
]

async def mutation(self, configuration: Configuration,
state: State,
request: MutationRequest) -> MutationResponse:
responses = []
for operation in request.operations:
operation_name = operation.name
func = self.mutation_functions[operation_name]
args = operation.arguments if operation.arguments else {}
response = func(**args)
responses.append(response)
return MutationResponse(
operation_results=[
MutationOperationResults(
type="procedure",
result=response
) for response in responses
]
)

def register_query(self, func):
self.query_functions[func.__name__] = func
return func

def register_mutation(self, func):
self.mutation_functions[func.__name__] = func
return func


# # TODO: Handle Pydantic types in the introspection
# class Tmp(BaseModel):
# x: str
# y: int
# z: float

connector = FunctionConnector()


@connector.register_query
def do_the_thing(x: int) -> str:
print(x)
return "Hello World"


@connector.register_mutation
def some_mutation_function(arg1: str, arg2: int) -> str:
# Mutation function implementation
return f"Hey {arg1} {arg2}"


if __name__ == "__main__":
start(connector)
38 changes: 38 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
annotated-types==0.6.0
anyio==4.3.0
asgiref==3.8.1
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
Deprecated==1.2.14
fastapi==0.110.2
googleapis-common-protos==1.63.0
grpcio==1.62.2
h11==0.14.0
hasura-ndc==0.11
idna==3.7
importlib-metadata==7.0.0
opentelemetry-api==1.24.0
opentelemetry-exporter-otlp-proto-common==1.24.0
opentelemetry-exporter-otlp-proto-grpc==1.24.0
opentelemetry-instrumentation==0.45b0
opentelemetry-instrumentation-asgi==0.45b0
opentelemetry-instrumentation-fastapi==0.45b0
opentelemetry-instrumentation-logging==0.45b0
opentelemetry-instrumentation-requests==0.45b0
opentelemetry-propagator-b3==1.24.0
opentelemetry-proto==1.24.0
opentelemetry-sdk==1.24.0
opentelemetry-semantic-conventions==0.45b0
opentelemetry-util-http==0.45b0
protobuf==4.25.3
pydantic==2.7.0
pydantic_core==2.18.1
requests==2.31.0
sniffio==1.3.1
starlette==0.37.2
typing_extensions==4.11.0
urllib3==2.2.1
uvicorn==0.29.0
wrapt==1.16.0
zipp==3.18.1

0 comments on commit fc84b77

Please sign in to comment.