I have a API Gateway service based on FastAPI and some specific services (like plugins) to connect with it. One of them - Auth service dealing with user accounts and access-tokens.
For example Auth service wants to tell AG about new functionality he provides and to register new endpoint in AG at runtime.
I see the following steps:
Auth creates new endpoint in AG, /new_endpoint for example;
All the traffic going to http://AG/new_endpoint will be redirected to http://Auth/...
I looked at the method FastAPI.add_api_route to add new endpoint. It works at runtime - I checked using curl.
There is no effect after refreshing http://AG/docs page because OpenAPI schema is cached.
I would like to re-generate OpenAPI schema and see /new_endpoint on the OpenAPI page.
I think I found the solution how to re-generate OpenAPI schema.
Drop cache app.openapi_schema = None
Re-generate schema app.setup()
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
app = FastAPI()
class NewEndpointResponse(BaseModel):
status: str
method: str
url_path: str
async def catch_all(request: Request) -> JSONResponse:
"""
Your new endpoint handler
"""
# some logic to interact with Auth-service
# like: requests.get("http://Auth/...")
res = NewEndpointResponse(status="OK", method=request.method, url_path=request.url.path)
return JSONResponse(res.dict(), status_code=200)
class EndpointRegisterDTO(BaseModel):
endpoint: str = "/new_endpoint"
method: str = "GET"
name: str = "Extra Functionality"
#app.post("/register/endpoint")
async def add_endpoint(request: EndpointRegisterDTO):
"""
Adds new endpoint at runtime
"""
app.add_api_route(
request.endpoint,
catch_all,
methods=[request.method],
name=request.name,
response_model=NewEndpointResponse)
app.openapi_schema = None
app.setup()
return {"status": "OK"}
Open http://AG/docs. Only one endpoint is available.
Press "Try it out" and do POST /register/endpoint with suggested parameters.
Refresh http://AG/docs - now you can see /new_endpoint.
Call GET /new_endpoint and check that response is correct.
The solution is ugly a bit, but it works!
I think it's bloody hard to debug it!
I'm new-ish to Python. I spend a fair of time working with the Azure Python SDK's. There is a pattern in these SDK's that works well for me. I want to duplicate the pattern below using the GitHub REST API, but can't find a GitHub "Service Client" or the like in the API docs.
There is an idea of RestClient in the docs, but its server code, not REST API syntax.
How do I create a service client one time, then use it for multiple REST calls?
Example Azure Python SDK pattern:
Create a client to interact with a specific service
Feed the connection string, api key, credential, etc. one time
Pass the client to other functions throughout the program
from azure.data.tables import TableClient
def create_table_service(conn_string, table_name):
try:
table_service = TableClient.from_connection_string(conn_string, table_name)
except Exception as e:
logging.error(f'##### Error creating Table Service Client: {e}')
return table_service
def update_feed_states(table_client, table_name, entity):
try:
test = table_client.insert_or_replace_entity(table_name, entity)
return test
except Exception as e:
logging.error(f'##### Could not insert or update in : {table_name}: {e}')
raise e
#----------------------------------------------
conn_string = '<connection-string-here>'
table_name = '<table-name-here>'
entity = {
'PartitionKey': 'foo',
'RowKey': 'bar',
'last_mod': '10/22/22'
}
#------------------------------------------------------
table_client = create_table_service(conn_string, table_name)
table_updates = update_feed_states(table_client, table_name, entity)
How do we post a GraphQL request through AWS AppSync using boto?
Ultimately I'm trying to mimic a mobile app accessing our stackless/cloudformation stack on AWS, but with python. Not javascript or amplify.
The primary pain point is authentication; I've tried a dozen different ways already. This the current one, which generates a "401" response with "UnauthorizedException" and "Permission denied", which is actually pretty good considering some of the other messages I've had. I'm now using the 'aws_requests_auth' library to do the signing part. I assume it authenticates me using the stored /.aws/credentials from my local environment, or does it?
I'm a little confused as to where and how cognito identities and pools will come into it. eg: say I wanted to mimic the sign-up sequence?
Anyways the code looks pretty straightforward; I just don't grok the authentication.
from aws_requests_auth.boto_utils import BotoAWSRequestsAuth
APPSYNC_API_KEY = 'inAppsyncSettings'
APPSYNC_API_ENDPOINT_URL = 'https://aaaaaaaaaaaavzbke.appsync-api.ap-southeast-2.amazonaws.com/graphql'
headers = {
'Content-Type': "application/graphql",
'x-api-key': APPSYNC_API_KEY,
'cache-control': "no-cache",
}
query = """{
GetUserSettingsByEmail(email: "john#washere"){
items {name, identity_id, invite_code}
}
}"""
def test_stuff():
# Use the library to generate auth headers.
auth = BotoAWSRequestsAuth(
aws_host='aaaaaaaaaaaavzbke.appsync-api.ap-southeast-2.amazonaws.com',
aws_region='ap-southeast-2',
aws_service='appsync')
# Create an http graphql request.
response = requests.post(
APPSYNC_API_ENDPOINT_URL,
json={'query': query},
auth=auth,
headers=headers)
print(response)
# this didn't work:
# response = requests.post(APPSYNC_API_ENDPOINT_URL, data=json.dumps({'query': query}), auth=auth, headers=headers)
Yields
{
"errors" : [ {
"errorType" : "UnauthorizedException",
"message" : "Permission denied"
} ]
}
It's quite simple--once you know. There are some things I didn't appreciate:
I've assumed IAM authentication (OpenID appended way below)
There are a number of ways for appsync to handle authentication. We're using IAM so that's what I need to deal with, yours might be different.
Boto doesn't come into it.
We want to issue a request like any regular punter, they don't use boto, and neither do we. Trawling the AWS boto docs was a waste of time.
Use the AWS4Auth library
We are going to send a regular http request to aws, so whilst we can use python requests they need to be authenticated--by attaching headers.
And, of course, AWS auth headers are special and different from all others.
You can try to work out how to do it
yourself, or you can go looking for someone else who has already done it: Aws_requests_auth, the one I started with, probably works just fine, but I have ended up with AWS4Auth. There are many others of dubious value; none endorsed or provided by Amazon (that I could find).
Specify appsync as the "service"
What service are we calling? I didn't find any examples of anyone doing this anywhere. All the examples are trivial S3 or EC2 or even EB which left uncertainty. Should we be talking to api-gateway service? Whatsmore, you feed this detail into the AWS4Auth routine, or authentication data. Obviously, in hindsight, the request is hitting Appsync, so it will be authenticated by Appsync, so specify "appsync" as the service when putting together the auth headers.
It comes together as:
import requests
from requests_aws4auth import AWS4Auth
# Use AWS4Auth to sign a requests session
session = requests.Session()
session.auth = AWS4Auth(
# An AWS 'ACCESS KEY' associated with an IAM user.
'AKxxxxxxxxxxxxxxx2A',
# The 'secret' that goes with the above access key.
'kwWxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxgEm',
# The region you want to access.
'ap-southeast-2',
# The service you want to access.
'appsync'
)
# As found in AWS Appsync under Settings for your endpoint.
APPSYNC_API_ENDPOINT_URL = 'https://nqxxxxxxxxxxxxxxxxxxxke'
'.appsync-api.ap-southeast-2.amazonaws.com/graphql'
# Use JSON format string for the query. It does not need reformatting.
query = """
query foo {
GetUserSettings (
identity_id: "ap-southeast-2:8xxxxxxb-7xx4-4xx4-8xx0-exxxxxxx2"
){
user_name, email, whatever
}}"""
# Now we can simply post the request...
response = session.request(
url=APPSYNC_API_ENDPOINT_URL,
method='POST',
json={'query': query}
)
print(response.text)
Which yields
# Your answer comes as a JSON formatted string in the text attribute, under data.
{"data":{"GetUserSettings":{"user_name":"0xxxxxxx3-9102-42f0-9874-1xxxxx7dxxx5"}}}
Getting credentials
To get rid of the hardcoded key/secret you can consume the local AWS ~/.aws/config and ~/.aws/credentials, and it is done this way...
# Use AWS4Auth to sign a requests session
session = requests.Session()
credentials = boto3.session.Session().get_credentials()
session.auth = AWS4Auth(
credentials.access_key,
credentials.secret_key,
boto3.session.Session().region_name,
'appsync',
session_token=credentials.token
)
...<as above>
This does seem to respect the environment variable AWS_PROFILE for assuming different roles.
Note that STS.get_session_token is not the way to do it, as it may try to assume a role from a role, depending where it keyword matched the AWS_PROFILE value. Labels in the credentials file will work because the keys are right there, but names found in the config file do not work, as that assumes a role already.
OpenID
In this scenario, all the complexity is transferred to the conversation with the openid connect provider. The hard stuff is all the auth hoops you jump through to get an access token, and thence using the refresh token to keep it alive. That is where all the real work lies.
Once you finally have an access token, assuming you have configured the "OpenID Connect" Authorization Mode in appsync, then you can, very simply, drop the access token into the header:
response = requests.post(
url="https://nc3xxxxxxxxxx123456zwjka.appsync-api.ap-southeast-2.amazonaws.com/graphql",
headers={"Authorization": ACCESS_TOKEN},
json={'query': "query foo{GetStuff{cat, dog, tree}}"}
)
You can set up an API key on the AppSync end and use the code below. This works for my case.
import requests
# establish a session with requests session
session = requests.Session()
# As found in AWS Appsync under Settings for your endpoint.
APPSYNC_API_ENDPOINT_URL = 'https://vxxxxxxxxxxxxxxxxxxy.appsync-api.ap-southeast-2.amazonaws.com/graphql'
# setup the query string (optional)
query = """query listItemsQuery {listItemsQuery {items {correlation_id, id, etc}}}"""
# Now we can simply post the request...
response = session.request(
url=APPSYNC_API_ENDPOINT_URL,
method='POST',
headers={'x-api-key': '<APIKEYFOUNDINAPPSYNCSETTINGS>'},
json={'query': query}
)
print(response.json()['data'])
Building off Joseph Warda's answer you can use the class below to send AppSync commands.
# fileName: AppSyncLibrary
import requests
class AppSync():
def __init__(self,data):
endpoint = data["endpoint"]
self.APPSYNC_API_ENDPOINT_URL = endpoint
self.api_key = data["api_key"]
self.session = requests.Session()
def graphql_operation(self,query,input_params):
response = self.session.request(
url=self.APPSYNC_API_ENDPOINT_URL,
method='POST',
headers={'x-api-key': self.api_key},
json={'query': query,'variables':{"input":input_params}}
)
return response.json()
For example in another file within the same directory:
from AppSyncLibrary import AppSync
APPSYNC_API_ENDPOINT_URL = {YOUR_APPSYNC_API_ENDPOINT}
APPSYNC_API_KEY = {YOUR_API_KEY}
init_params = {"endpoint":APPSYNC_API_ENDPOINT_URL,"api_key":APPSYNC_API_KEY}
app_sync = AppSync(init_params)
mutation = """mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
content
}
}
"""
input_params = {
"content":"My first post"
}
response = app_sync.graphql_operation(mutation,input_params)
print(response)
Note: This requires you to activate API access for your AppSync API. Check this AWS post for more details.
graphql-python/gql supports AWS AppSync since version 3.0.0rc0.
It supports queries, mutation and even subscriptions on the realtime endpoint.
The documentation is available here
Here is an example of a mutation using the API Key authentication:
import asyncio
import os
import sys
from urllib.parse import urlparse
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.appsync_auth import AppSyncApiKeyAuthentication
# Uncomment the following lines to enable debug output
# import logging
# logging.basicConfig(level=logging.DEBUG)
async def main():
# Should look like:
# https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.REGION.amazonaws.com/graphql
url = os.environ.get("AWS_GRAPHQL_API_ENDPOINT")
api_key = os.environ.get("AWS_GRAPHQL_API_KEY")
if url is None or api_key is None:
print("Missing environment variables")
sys.exit()
# Extract host from url
host = str(urlparse(url).netloc)
auth = AppSyncApiKeyAuthentication(host=host, api_key=api_key)
transport = AIOHTTPTransport(url=url, auth=auth)
async with Client(
transport=transport, fetch_schema_from_transport=False,
) as session:
query = gql(
"""
mutation createMessage($message: String!) {
createMessage(input: {message: $message}) {
id
message
createdAt
}
}"""
)
variable_values = {"message": "Hello world!"}
result = await session.execute(query, variable_values=variable_values)
print(result)
asyncio.run(main())
I am unable to add a comment due to low rep, but I just want to add that I tried the accepted answer and it didn't work. I was getting an error saying my session_token is invalid. Probably because I was using AWS Lambda.
I got it to work pretty much exactly, but by adding to the session token parameter of the aws4auth object. Here's the full piece:
import requests
import os
from requests_aws4auth import AWS4Auth
def AppsyncHandler(event, context):
# These are env vars that are always present in an AWS Lambda function
# If not using AWS Lambda, you'll need to add them manually to your env.
access_id = os.environ.get("AWS_ACCESS_KEY_ID")
secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
session_token = os.environ.get("AWS_SESSION_TOKEN")
region = os.environ.get("AWS_REGION")
# Your AppSync Endpoint
api_endpoint = os.environ.get("AppsyncConnectionString")
resource = "appsync"
session = requests.Session()
session.auth = AWS4Auth(access_id,
secret_key,
region,
resource,
session_token=session_token)
The rest is the same.
Hope this Helps Everyone
import requests
import json
import os
from dotenv import load_dotenv
load_dotenv(".env")
class AppSync(object):
def __init__(self,data):
endpoint = data["endpoint"]
self.APPSYNC_API_ENDPOINT_URL = endpoint
self.api_key = data["api_key"]
self.session = requests.Session()
def graphql_operation(self,query,input_params):
response = self.session.request(
url=self.APPSYNC_API_ENDPOINT_URL,
method='POST',
headers={'x-api-key': self.api_key},
json={'query': query,'variables':{"input":input_params}}
)
return response.json()
def main():
APPSYNC_API_ENDPOINT_URL = os.getenv("APPSYNC_API_ENDPOINT_URL")
APPSYNC_API_KEY = os.getenv("APPSYNC_API_KEY")
init_params = {"endpoint":APPSYNC_API_ENDPOINT_URL,"api_key":APPSYNC_API_KEY}
app_sync = AppSync(init_params)
mutation = """
query MyQuery {
getAccountId(id: "5ca4bbc7a2dd94ee58162393") {
_id
account_id
limit
products
}
}
"""
input_params = {}
response = app_sync.graphql_operation(mutation,input_params)
print(json.dumps(response , indent=3))
main()
I want to provide parameters for a GET request/ API call to the AYLIEN text analytics API. I can set the headers for the key and ID as authorization and the call itself works (my usage statistics of the API increase after running the code), but I don't know how to provide the text to analyze as a parameter.
def customScript(){
def connection = new
URL("https://api.aylien.com/api/v1/sentiment").openConnection() as
HttpURLConnection
connection.requestMethod = 'GET'
// set headers
connection.setRequestProperty('X-AYLIEN-TextAPI-Application-Key','//mykey')
connection.setRequestProperty('X-AYLIEN-TextAPI-Application-ID', '//myid')
// get the response code - automatically sends the request
println connection.responseCode + ": " + connection.inputStream.text
}
In a GET request, the parameters are sent as part of the URL.
For example, if you want to add the parameter id=23, you can change the code to:
def connection = new
URL("https://api.aylien.com/api/v1/sentiment?id=23").openConnection() as
HttpURLConnection
I am creating a self built Python chatbot that does not use a chatbot platform such as Dialogflow. The issue is that there is no easy integration with messaging apps such as Messenger to connect it too. I am trying to create a webhook to Messenger using the Messenger Send API. I am looking at the documentation and it shows me how to request a POST api call. However when I look at examples online they all seem to deal with json values called "entry" and "messaging" which I can't find anywhere and can't seem to see why it is necessary. I was wondering how exactly the input body of a Messenger Send API looks like so I can call it appropriately and what json objects are in its body. This is what I have so far from following online examples. I am using Flask. And am using Postman to test this
#app.route("/webhook", methods=['GET','POST'])
def listen():
if request.method == 'GET':
return verify_webhook(request)
if request.method == 'POST':
payload = request.json
event = payload['entry'][0]['messaging']
for x in event:
if is_user_message(x):
text = x['message']['text']
sender_id = x['sender']['id']
respond(sender_id, text)
return "ok"
Below is what I think the body of the request looks like:
{
"object":"page",
"entry":[
{
"id":"1234",
"time":1458692752478,
"messaging":[
{
"message":{
"text":"book me a cab"
},
"sender":{
"id":"1234"
}
}
]
}
]
}
But it is unable to read this and gives me an error of:
File"/Users/raphael******/Documents/*****_Project_Raphael/FacebookWebHookEdition.py", line 42, in listen
event = payload['entry'][0]['messaging']
TypeError: 'NoneType' object is not subscriptable
Where am I going wrong that the webhook is not registering the body correctly as json objects?
Here is how we do it:
# GET: validate facebook token
if request.method == 'GET':
valid = messenger.verify_request(request)
if not valid:
abort(400, 'Invalid Facebook Verify Token')
return valid
# POST: process message
output = request.get_json()
if 'entry' not in output:
return 'OK'
for entry in output['entry']:
if 'messaging' not in entry:
continue
for item in entry['messaging']:
# delivery notification (skip)
if 'delivery' in item:
continue
# get user
user = item['sender'] if 'sender' in item else None
if not user:
continue
else:
user_id = user['id']
# handle event
messenger.handle(user_id, item)
# message processed
return 'OK'
EDIT:
If you are using postman, please make sure to also set Content-Type header to application/json, otherwise Flask can't decode it with request.json. I guess that's where None comes from in your case.