|
| 1 | +""" |
| 2 | +functions.py |
| 3 | +
|
| 4 | +This is an example of how you can use the Python SDK's built-in Function connector to easily write Python code. |
| 5 | +When you add a Python Lambda connector to your Hasura project, this file is generated for you! |
| 6 | +
|
| 7 | +In this file you'll find code examples that will help you get up to speed with the usage of the Hasura lambda connector. |
| 8 | +If you are an old pro and already know what is going on you can get rid of these example functions and start writing your own code. |
| 9 | +""" |
1 | 10 | from hasura_ndc import start
|
2 |
| -from hasura_ndc.instrumentation import with_active_span |
3 |
| -from opentelemetry.trace import get_tracer |
| 11 | +from hasura_ndc.instrumentation import with_active_span # If you aren't planning on adding additional tracing spans, you don't need this! |
| 12 | +from opentelemetry.trace import get_tracer # If you aren't planning on adding additional tracing spans, you don't need this either! |
4 | 13 | from hasura_ndc.function_connector import FunctionConnector
|
5 |
| -from pydantic import BaseModel |
| 14 | +from pydantic import BaseModel # You only need this import if you plan to have complex inputs/outputs, which function similar to how frameworks like FastAPI do |
6 | 15 |
|
7 | 16 | connector = FunctionConnector()
|
8 |
| -tracer = get_tracer("ndc-sdk-python.server") |
9 | 17 |
|
| 18 | +# This is an example of a simple function that can be added onto the graph |
| 19 | +@connector.register_query # This is how you register a query |
| 20 | +def hello(name: str) -> str: |
| 21 | + return f"Hello {name}" |
| 22 | + |
| 23 | +# You can use Nullable parameters, but they must default to None |
| 24 | +# The FunctionConnector also doesn't care if your functions are sync or async, so use whichever you need! |
10 | 25 | @connector.register_query
|
11 |
| -def do_the_thing(x: int): |
12 |
| - return f"Hello World {x}" |
| 26 | +async def nullable_hello(name: str | None = None) -> str: |
| 27 | + return f"Hello {name if name is not None else 'world'}" |
13 | 28 |
|
| 29 | +# Parameters that are untyped accept any scalar type, arrays, or null and are treated as JSON. |
| 30 | +# Untyped responses or responses with indeterminate types are treated as JSON as well! |
| 31 | +@connector.register_mutation # This is how you register a mutation |
| 32 | +def some_mutation_function(any_type_param): |
| 33 | + return any_type_param |
14 | 34 |
|
15 |
| -@connector.register_mutation |
16 |
| -def some_mutation_function(arg1: str, |
17 |
| - arg2: int): |
18 |
| - return f"Hey {arg1} {arg2}" |
| 35 | +# Similar to frameworks like FastAPI, you can use Pydantic Models for inputs and outputs |
| 36 | +class Pet(BaseModel): |
| 37 | + name: str |
| 38 | + |
| 39 | +class Person(BaseModel): |
| 40 | + name: str |
| 41 | + pets: list[Pet] | None = None |
19 | 42 |
|
20 | 43 | @connector.register_query
|
21 |
| -async def my_query(x: str) -> str: |
22 |
| - return await with_active_span( |
23 |
| - tracer, |
24 |
| - "My Span", |
25 |
| - lambda span: f"My string is {x}", |
26 |
| - {"attr": "value"} |
27 |
| - ) |
| 44 | +def greet_person(person: Person) -> str: |
| 45 | + greeting = f"Hello {person.name}!" |
| 46 | + if person.pets is not None: |
| 47 | + for pet in person.pets: |
| 48 | + greeting += f" And hello to {pet.name}.." |
| 49 | + else: |
| 50 | + greeting += f" I see you don't have any pets." |
| 51 | + return greeting |
| 52 | + |
| 53 | +class ComplexType(BaseModel): |
| 54 | + lists: list[list] # This turns into a List of List's of any valid JSON! |
| 55 | + person: Person | None = None # This becomes a nullable attribute that accepts a person type from above |
| 56 | + x: int # You can also use integers |
| 57 | + y: float # As well as floats |
| 58 | + z: bool # And booleans |
28 | 59 |
|
| 60 | +# When the outputs are typed with Pydantic models you can select which attributes you want returned! |
29 | 61 | @connector.register_query
|
30 |
| -async def my_query2(x: str): |
31 |
| - async def f(span): |
32 |
| - # return f"My string is {x}" |
33 |
| - return { |
34 |
| - "hey": "x", |
35 |
| - "var": x, |
36 |
| - 10.1: 10.1, |
37 |
| - "dict": { |
38 |
| - 1.0: 10 |
39 |
| - }, |
40 |
| - "floatables": [1.234, 10, "yep"] |
41 |
| - } |
| 62 | +def complex_function(input: ComplexType) -> ComplexType: |
| 63 | + return input |
| 64 | + |
| 65 | +# This last section shows you how to add Otel tracing to any of your functions! |
| 66 | +tracer = get_tracer("ndc-sdk-python.server") # You only need a tracer if you plan to add additional Otel spans |
| 67 | + |
| 68 | +# Utilizing with_active_span allows the programmer to add Otel tracing spans |
| 69 | +@connector.register_query |
| 70 | +async def with_tracing(name: str) -> str: |
| 71 | + |
| 72 | + def do_some_more_work(_span, work_response): |
| 73 | + return f"Hello {name}, {work_response}" |
| 74 | + |
| 75 | + async def the_async_work_to_do(): |
| 76 | + # This isn't actually async work, but it could be! Perhaps a network call belongs here, the power is in your hands fellow programmer! |
| 77 | + return "That was a lot of work we did!" |
| 78 | + |
| 79 | + async def do_some_async_work(_span): |
| 80 | + work_response = await the_async_work_to_do() |
| 81 | + return await with_active_span( |
| 82 | + tracer, |
| 83 | + "Sync Work Span", |
| 84 | + lambda span: do_some_more_work(span, work_response), # Spans can wrap synchronous functions, and they can be nested for fine-grained tracing |
| 85 | + {"attr": "sync work attribute"} |
| 86 | + ) |
42 | 87 |
|
43 | 88 | return await with_active_span(
|
44 | 89 | tracer,
|
45 |
| - "My Span", |
46 |
| - f, |
47 |
| - {"attr": "value"} |
| 90 | + "Root Span that does some async work", |
| 91 | + do_some_async_work, # Spans can wrap asynchronous functions |
| 92 | + {"tracing-attr": "Additional attributes can be added to Otel spans by making use of with_active_span like this"} |
48 | 93 | )
|
49 | 94 |
|
50 | 95 |
|
|
0 commit comments