Introduce V2 of current and historical functions

This commit is contained in:
2024-11-03 23:18:06 -08:00
parent 27cd98ee52
commit ff8e182336
21 changed files with 655 additions and 428 deletions

0
wow_token/db/__init__.py Normal file
View File

28
wow_token/db/cache.py Normal file
View File

@@ -0,0 +1,28 @@
import datetime
from typing import Dict, List, Tuple
from wow_token.db.cached_range import CachedRange
from wow_token.db.trinity import Trinity
class Cache:
_cache : Dict[str, List[Tuple[datetime.datetime, int]]]
_db : 'Compacted'
def __init__(self, compacted_db: 'Compacted'):
self._db = compacted_db
self._cache = {}
def get_month(self, trinity: Trinity) -> List[Tuple[datetime.datetime, int]]:
current_time = datetime.datetime.now(datetime.UTC)
if isinstance(trinity.range, CachedRange):
raise NotImplementedError
current_month = trinity.range.month == current_time.month and trinity.range.year == current_time.year
if not current_month and str(trinity) in self._cache:
return self._cache[str(trinity)]
self._cache[str(trinity)] = self._db.ddb_get_data(trinity)
return self._cache[str(trinity)]

View File

@@ -0,0 +1,14 @@
class CachedRange:
_PRECOMPUTE_RANGES = ['30d', '90d', '6m', '1y', '2y', 'all']
# I despise magic strings but this is about as good as I can get without enum support
def __init__(self, _range: str):
if _range not in CachedRange._PRECOMPUTE_RANGES:
raise ValueError(f'Invalid range: {_range}')
self._range = _range
@property
def range(self):
return self._range
def __str__(self):
return self._range

84
wow_token/db/compacted.py Normal file
View File

@@ -0,0 +1,84 @@
import datetime
import os
from typing import List, Dict, Tuple, Union, Type
import boto3
from boto3.dynamodb.conditions import Key
from wow_token.db.trinity import Trinity
from wow_token.db.year_month import YearMonth
from wow_token.db.cache import Cache
from wow_token.region import Region
# TODO: Reduce Compacted Table Sprawl
REGION_MAP = {
'us-west-1': 'us-west-1',
'us-west-2': 'us-west-2',
'us-east-1': 'us-east-1',
'us-east-2': 'us-east-2',
'ap-south-1': 'eu-north-1',
'ap-northeast-3': 'ap-northeast-1',
'ap-northeast-2': 'ap-northeast-1',
'ap-southeast-1': 'ap-southeast-1',
'ap-southeast-2': 'ap-southeast-2',
'ap-northeast-1': 'ap-northeast-1',
'ca-central-1': 'us-east-1',
'eu-central-1': 'eu-north-1',
'eu-west-1': 'eu-west-1',
'eu-west-2': 'eu-west-1',
'eu-west-3': 'eu-west-3',
'eu-north-1': 'eu-north-1',
'sa-east-1': 'sa-east-1',
'eu-south-1': 'eu-north-1'
}
def _region_selector():
if os.environ['AWS_REGION'] in REGION_MAP:
local_region = REGION_MAP[os.environ['AWS_REGION']]
else:
local_region = 'eu-central-1'
return local_region
def _data_as_str(data: List[Tuple[datetime.datetime, int]]) -> List[Tuple[str, int]]:
data_as_str = []
for timestamp, price in data:
data_as_str.append((timestamp.isoformat(), price))
return data_as_str
class Compacted:
_cache : Cache
def __init__(self):
self._ddb = boto3.resource('dynamodb', region_name=_region_selector())
self._table = self._ddb.Table('wow-token-compacted')
self._cache = Cache(self)
def ddb_get_data(self, trinity: Trinity, _type: Union[Type[str], Type[datetime.datetime]] = datetime.datetime) -> Union[List[Tuple[datetime.datetime, int]], List[Tuple[str, int]]]:
data = []
response = self._table.query(
KeyConditionExpression=Key('region-flavor-timestamp').eq(str(trinity))
)
if response['Items']:
for timestamp, price in response['Items'][0]['data'].items():
date_time = datetime.datetime.fromtimestamp(int(timestamp), datetime.UTC)
if _type == str:
date_time = date_time.isoformat()
data.append((
date_time,
int(price)
))
return sorted(data, key=lambda x: x[0])
def get_month(self, trinity: Trinity, _type: Union[Type[str], Type[datetime.datetime]] = datetime.datetime) -> Union[List[Tuple[datetime.datetime, int]], List[Tuple[str, int]]]:
if _type == str:
return _data_as_str(self._cache.get_month(trinity))
return self._cache.get_month(trinity)
def get_precomputed_range(self, trinity: Trinity, _type: Union[Type[str], Type[datetime.datetime]] = datetime.datetime) -> Union[List[Tuple[datetime.datetime, int]], List[Tuple[str, int]]]:
if isinstance(trinity.range, YearMonth):
return self.get_month(trinity, _type=_type)
else:
return self.ddb_get_data(trinity, _type=_type)

64
wow_token/db/current.py Normal file
View File

@@ -0,0 +1,64 @@
import datetime
import os
from typing import List, Dict, Tuple
import boto3
from boto3.dynamodb.conditions import Key
from wow_token.flavor import Flavor
from wow_token.region import Region
REGION_MAP = {
'us-west-1': 'us-west-1',
'us-west-2': 'us-west-2',
'us-east-1': 'us-east-1',
'us-east-2': 'us-east-2',
'ap-south-1': 'eu-north-1',
'ap-northeast-3': 'ap-northeast-1',
'ap-northeast-2': 'ap-northeast-1',
'ap-southeast-1': 'ap-southeast-1',
'ap-southeast-2': 'ap-southeast-2',
'ap-northeast-1': 'ap-northeast-1',
'ca-central-1': 'us-east-1',
'eu-central-1': 'eu-north-1',
'eu-west-1': 'eu-west-1',
'eu-west-2': 'eu-west-1',
'eu-west-3': 'eu-west-3',
'eu-north-1': 'eu-north-1',
'sa-east-1': 'sa-east-1',
'eu-south-1': 'eu-north-1'
}
def _region_selector():
if os.environ['AWS_REGION'] in REGION_MAP:
local_region = REGION_MAP[os.environ['AWS_REGION']]
else:
local_region = 'eu-central-1'
return local_region
class Current:
def __init__(self):
self._ddb = boto3.resource('dynamodb', region_name=_region_selector())
self._tables = {
Flavor.RETAIL: self._ddb.Table('wow-token-price'),
Flavor.CLASSIC: self._ddb.Table('wow-token-classic-price'),
}
def _ddb_get_current_all(self, flavor: Flavor) -> Dict[Region, Tuple[datetime.datetime, int]]:
response = self._tables[flavor].scan()
data = {}
for item in response['Items']:
region = Region(item['region'])
data[region] = (
datetime.datetime.fromtimestamp(int(item['current_time']), datetime.UTC),
int(int(item['price']) / 10_000) # the raw copper value is what is stored in DynamoDB
)
return data
def get_current_all(self, flavor: Flavor) -> Dict[Region, Tuple[str, int]]:
data = {}
for region, (timestamp, _) in self._ddb_get_current_all(flavor).items():
data[region] = (timestamp.isoformat(), _)
return data

67
wow_token/db/recent.py Normal file
View File

@@ -0,0 +1,67 @@
import datetime
import os
from typing import List, Dict, Tuple
import boto3
from boto3.dynamodb.conditions import Key
from wow_token.flavor import Flavor
from wow_token.region import Region
REGION_MAP = {
'us-west-1': 'us-west-1',
'us-west-2': 'us-west-2',
'us-east-1': 'us-east-1',
'us-east-2': 'us-east-2',
'ap-south-1': 'eu-north-1',
'ap-northeast-3': 'ap-northeast-1',
'ap-northeast-2': 'ap-northeast-1',
'ap-southeast-1': 'ap-southeast-1',
'ap-southeast-2': 'ap-southeast-2',
'ap-northeast-1': 'ap-northeast-1',
'ca-central-1': 'us-east-1',
'eu-central-1': 'eu-north-1',
'eu-west-1': 'eu-west-1',
'eu-west-2': 'eu-west-1',
'eu-west-3': 'eu-west-3',
'eu-north-1': 'eu-north-1',
'sa-east-1': 'sa-east-1',
'eu-south-1': 'eu-north-1'
}
def _region_selector():
if os.environ['AWS_REGION'] in REGION_MAP:
local_region = REGION_MAP[os.environ['AWS_REGION']]
else:
local_region = 'eu-central-1'
return local_region
class Recent:
def __init__(self):
self._ddb = boto3.resource('dynamodb', region_name=_region_selector())
self._tables = {
Flavor.RETAIL: self._ddb.Table('wow-token-price-recent'),
Flavor.CLASSIC: self._ddb.Table('wow-token-classic-price-recent'),
}
def get_after_unix_timestamp(self, flavor: Flavor, region: Region, timestamp: int) -> List[Tuple[str, int]]:
response = self._tables[flavor].query(
KeyConditionExpression=(
Key('region').eq(region.value) &
Key('timestamp').gte(timestamp)
)
)
data = []
last_price = 0
for item in response['Items']:
price = int(int(item['price']) / 10_000) # the raw copper value is what is stored in DynamoDB
if last_price != price:
item_time = datetime.datetime.fromtimestamp(int(item['timestamp']), datetime.UTC).isoformat()
data.append((
item_time,
price
))
last_price = price
return data

26
wow_token/db/trinity.py Normal file
View File

@@ -0,0 +1,26 @@
from wow_token.db.cached_range import CachedRange
from wow_token.db.year_month import YearMonth
from wow_token.flavor import Flavor
from wow_token.region import Region
class Trinity:
def __init__(self, _region: Region, _flavor: Flavor, _range: CachedRange | YearMonth):
self._region = _region
self._flavor = _flavor
self._range = _range
@property
def region(self) -> Region:
return self._region
@property
def flavor(self) -> Flavor:
return self._flavor
@property
def range(self) -> CachedRange | YearMonth:
return self._range
def __str__(self):
return f"{self._region.value}-{self._flavor.value}-{self._range}"

View File

@@ -0,0 +1,23 @@
class YearMonth:
# I really don't like how this class is named and used but
# past me is my own worst enemy and used it to make sorting on Dynamo easier
VALID_YEARS = [2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030]
VALID_MONTHS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
def __init__(self, year: int, month: int):
if year not in YearMonth.VALID_YEARS:
raise ValueError(f'Invalid year: {year}')
if month not in YearMonth.VALID_MONTHS:
raise ValueError(f'Invalid month: {month}')
self._year = year
self._month = month
@property
def month(self) -> int:
return self._month
@property
def year(self) -> int:
return self._year
def __str__(self):
return f'{self._year}-{self._month}'