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)