wowtoken.app-backend/wow-token-updater.py

272 lines
8.7 KiB
Python

import datetime
import time
import boto3
import json
import os
import requests
local_region = os.environ['AWS_REGION']
dynamo_client = boto3.client('dynamodb', region_name=local_region)
timestream_client = boto3.client('timestream-write', region_name=local_region)
tables = {
'retail': {
'recent': 'wow-token-price-recent',
'current': 'wow-token-price',
'timestream': 'wow-token-price-history'
},
'classic': {
'recent': 'wow-token-classic-price-recent',
'current': 'wow-token-classic-price',
'timestream': 'wow-token-classic-price-history'
}
}
# a lambda handler to handle eventbridge triggered vents
def lambda_handler(event, context):
flavors = ['retail', 'classic']
regions = ['us', 'eu', 'kr', 'tw']
timestamp = int(datetime.datetime.utcnow().timestamp())
for flavor in flavors:
flavor_handler(flavor, regions, timestamp)
def flavor_handler(flavor: str, regions: list, timestamp: int) -> None:
for region in regions:
if flavor == 'retail':
namespace = 'dynamic'
else:
namespace = 'dynamic-classic'
live_price = get_token_price_from_blizzard(region, namespace)
print(f"{flavor} {region} live price {live_price}")
print(f'Updating {region.upper()}...')
update_token_price(flavor, region, timestamp, live_price)
def update_token_price(flavor: str, region: str, current_time_epoch: int, live_price: int) -> None:
stored_price = get_stored_price(flavor, region)
regional_item = get_regional_update_item(flavor, region)
print(f'Current live price {live_price}')
print(f'Current stored price {stored_price}')
regional_price = int(regional_item['price']['S'])
print(f'Current regional price {regional_price}')
if stored_price != live_price:
# If the stored price is not the same as the live price, nor the regional price
# assume no other Lambda has updated it and update it
print(f"Stored price is differing from the live price, updating all databases")
update_stored_token_price(flavor, region, live_price, current_time_epoch)
update_recent_token_price(flavor, region, live_price, current_time_epoch)
update_regional_price(flavor, region, live_price, current_time_epoch)
update_timestream_token_price(flavor, region, live_price, current_time_epoch)
if (stored_price == live_price) and (regional_price != stored_price):
print(f"Stored price differs from regional price but not live price, updating regional databases")
update_regional_price(flavor, region, live_price, current_time_epoch)
update_timestream_token_price(flavor, region, live_price, current_time_epoch)
else:
print(f"Price hasn't changed for {flavor} {region.upper()}")
# update the current price records in DynamoDB
def update_stored_token_price(flavor: str, region: str, price: int, current_time_epoch: int) -> None:
dynamo_client.update_item(
TableName=tables[flavor]['current'],
Key={
'region': {
'S': region
}
},
UpdateExpression='SET price = :p, current_time = :t',
ExpressionAttributeValues={
':p': {
'S': str(price)
},
':t': {
'S': str(current_time_epoch)
}
}
# ReturnValues="UPDATED_NEW"
)
print(f'Updated {flavor} {region.upper()} price to {price}')
def update_regional_price(flavor: str, region: str, price: int, current_time_epoch: int) -> None:
if flavor == 'retail':
key = region
else:
key = f"{flavor}-{region}"
dynamo_client.update_item(
TableName='wow-token-regional',
Key={
'region': {
'S': key
}
},
UpdateExpression='SET price = :p, current_time = :t',
ExpressionAttributeValues={
':p': {
'S': str(price)
},
':t': {
'S': str(current_time_epoch)
}
}
# ReturnValues="UPDATED_NEW"
)
print(f'Updated regional {flavor} {region.upper()} price to {price}')
def create_regional_item(flavor: str, region: str) -> None:
print(f"Creating default regional item in {flavor} {region}")
if flavor == 'retail':
key = region
else:
key = f"{flavor}-{region}"
dynamo_client.put_item(
TableName='wow-token-regional',
Item={
'region': {
'S': key
},
'price': {
'S': str(1)
},
'timestamp': {
'N': str(1)
}
}
)
# add a record to the recent token price table in DynamoDB
def update_recent_token_price(flavor: str, region: str, price: int, current_time_epoch: int) -> None:
dynamo_client.put_item(
TableName=tables[flavor]['recent'],
Item={
'region': {
'S': region
},
'price': {
'S': str(price)
},
'timestamp': {
'N': str(current_time_epoch)
},
'expire': {
'N': str(current_time_epoch + (60 * 60 * 24 * 31 * 12))
}
}
# ReturnValues="UPDATED_NEW"
)
print(f'Added {region.upper()} price {price} to {tables[flavor]["recent"]} table')
def update_timestream_token_price(flavor: str, region: str, price: int, current_time_epoch: int) -> None:
record_inserted = False
while not record_inserted:
try:
print('Attempting to write to Timestream')
timestream_client.write_records(
DatabaseName=tables[flavor]['timestream'],
TableName=f'{region}-price-history',
Records=[
build_timestream_record(region, price, current_time_epoch),
]
)
record_inserted = True
except Exception as e:
print(f'Error writing to Timestream: {e}')
time.sleep(2)
print(f'Updated {flavor} {region.upper()} price to {price} in Timestream')
def build_timestream_record(region: str, price: int, current_time_epoch: int) -> dict:
return {
'Dimensions': [
{
'Name': 'region',
'Value': region,
'DimensionValueType': 'VARCHAR'
}
],
'MeasureName': 'price',
'MeasureValue': str(price),
'MeasureValueType': 'BIGINT',
'Time': str(current_time_epoch),
'TimeUnit': 'SECONDS',
}
# get the current stored token price from dynamodb
def get_stored_price(flavor: str, region: str) -> int:
response = dynamo_client.get_item(
TableName=tables[flavor]['current'],
Key={
'region': {
'S': region
}
}
)
return int(response['Item']['price']['S'])
def get_regional_update_item(flavor: str, region: str) -> dict:
if flavor == 'retail':
key = region
else:
key = f"{flavor}-{region}"
response = dynamo_client.get_item(
TableName='wow-token-regional',
Key={
'region': {
'S': key
}
}
)
if 'Item' in response:
return response['Item']
else:
create_regional_item(flavor, region)
time.sleep(5)
return get_regional_update_item(flavor, region)
def get_combined_live_price(game_flavor: str) -> dict:
if game_flavor == 'retail':
namespace = 'dynamic'
else:
namespace = 'dynamic-classic'
return {
'us': get_token_price_from_blizzard('us', namespace),
'eu': get_token_price_from_blizzard('eu', namespace),
'kr': get_token_price_from_blizzard('kr', namespace),
'tw': get_token_price_from_blizzard('tw', namespace)
}
def get_token_price_from_blizzard(region: str, namespace: str) -> int:
api_endpoint = f'https://{region}.api.blizzard.com/data/wow/token/index'
params = {'namespace': f'{namespace}-{region}'}
headers = {'Authorization': f'Bearer {get_oauth_token()}'}
response = requests.get(api_endpoint, params=params, headers=headers)
response.raise_for_status()
return int(response.json()['price'])
def get_oauth_token() -> str:
url = 'https://us.battle.net/oauth/token'
payload = {
'grant_type': 'client_credentials',
'client_id': os.environ.get('BLIZZARD_CLIENT_ID'),
'client_secret': os.environ.get('BLIZZARD_CLIENT_SECRET')
}
response = requests.post(url, data=payload)
response.raise_for_status()
return json.loads(response.text)['access_token']
if __name__ == '__main__':
lambda_handler(None, None)