generated from nathanwoodburn/python-webserver-template
This commit is contained in:
parent
0ead851279
commit
e8d5dc8f9f
49
cache.py
Normal file
49
cache.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from functools import wraps
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
def file_cache(folder="cache", ttl=300):
|
||||||
|
"""
|
||||||
|
Decorator to cache function results in the specified folder with a TTL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder (str): Directory where cached files will be stored.
|
||||||
|
ttl (int): Time-to-live for the cache in seconds.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
os.makedirs(folder)
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Create a unique cache key based on the function name and arguments
|
||||||
|
cache_key = hashlib.md5(
|
||||||
|
f"{func.__name__}-{args}-{kwargs}".encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
cache_file = os.path.join(folder, f"{cache_key}.json")
|
||||||
|
|
||||||
|
# Check if cache exists and is valid
|
||||||
|
if os.path.exists(cache_file):
|
||||||
|
try:
|
||||||
|
with open(cache_file, "r") as f:
|
||||||
|
cached_data = json.load(f)
|
||||||
|
# Check if the cache has expired
|
||||||
|
if time() - cached_data["timestamp"] < ttl:
|
||||||
|
return cached_data["result"]
|
||||||
|
except (IOError, ValueError, KeyError):
|
||||||
|
pass # In case of error, re-compute the result
|
||||||
|
|
||||||
|
# Call the function and cache the result
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
try:
|
||||||
|
with open(cache_file, "w") as f:
|
||||||
|
json.dump({"timestamp": time(), "result": result}, f)
|
||||||
|
except (IOError, TypeError) as e:
|
||||||
|
print(f"Warning: Could not cache result: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
1
cache/10118a51009b13b2592c87579e15e61e.json
vendored
Normal file
1
cache/10118a51009b13b2592c87579e15e61e.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363360.6520803, "result": "120"}
|
1
cache/1ccff5c6f117409fea0c861aa44b8e62.json
vendored
Normal file
1
cache/1ccff5c6f117409fea0c861aa44b8e62.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363478.77673, "result": 1.14}
|
1
cache/29409a8a40dd2d547a7a44b8f6758f54.json
vendored
Normal file
1
cache/29409a8a40dd2d547a7a44b8f6758f54.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363478.8822353, "result": 4.16}
|
1
cache/4104ed0427efe63d4ca0dead970a4391.json
vendored
Normal file
1
cache/4104ed0427efe63d4ca0dead970a4391.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363358.2618918, "result": 240.01}
|
1
cache/598f5dbf97fb0d45cbc6e1a5b0a3b575.json
vendored
Normal file
1
cache/598f5dbf97fb0d45cbc6e1a5b0a3b575.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363355.9661899, "result": 226.21}
|
1
cache/6eec370e2713cfc84c84e1080b8a191a.json
vendored
Normal file
1
cache/6eec370e2713cfc84c84e1080b8a191a.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363361.094433, "result": 120.0}
|
1
cache/94ac30c93587c50252ac382a8d02257f.json
vendored
Normal file
1
cache/94ac30c93587c50252ac382a8d02257f.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363355.9663823, "result": 1.47422459502}
|
1
cache/a071d7bdda25c22e42ad7840f17c4b0e.json
vendored
Normal file
1
cache/a071d7bdda25c22e42ad7840f17c4b0e.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363357.525003, "result": 0.999445}
|
1
cache/a0ee60913ba556f39d128e7d7249e788.json
vendored
Normal file
1
cache/a0ee60913ba556f39d128e7d7249e788.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363478.8832045, "result": [{"mint": "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", "balance": 0.039815492, "price": 240.01, "value": 9.55611623492, "name": "Jupiter Staked SOL", "symbol": "jupsol"}, {"mint": "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", "balance": 2.402337, "price": 4.16, "value": 9.99372192, "name": "Jupiter Perpetuals Liquidity Provider Token", "symbol": "jlp"}]}
|
1
cache/b81351778df9f812bbd75ee85a7a073e.json
vendored
Normal file
1
cache/b81351778df9f812bbd75ee85a7a073e.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363478.7767982, "result": 115.43342152993999}
|
1
cache/ccf2a009e56f1b05d471a55d9c9ea8ea.json
vendored
Normal file
1
cache/ccf2a009e56f1b05d471a55d9c9ea8ea.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"timestamp": 1733363419.3752136, "result": 82.815227}
|
4
main.py
4
main.py
@ -4,6 +4,7 @@ import server
|
|||||||
from gunicorn.app.base import BaseApplication
|
from gunicorn.app.base import BaseApplication
|
||||||
import os
|
import os
|
||||||
import dotenv
|
import dotenv
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
class GunicornApp(BaseApplication):
|
class GunicornApp(BaseApplication):
|
||||||
@ -23,6 +24,7 @@ class GunicornApp(BaseApplication):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@ -37,6 +39,8 @@ if __name__ == '__main__':
|
|||||||
'threads': threads,
|
'threads': threads,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
threading.Thread(target=server.update_data).start()
|
||||||
|
|
||||||
gunicorn_app = GunicornApp(server.app, options)
|
gunicorn_app = GunicornApp(server.app, options)
|
||||||
print(f'Starting server with {workers} workers and {threads} threads', flush=True)
|
print(f'Starting server with {workers} workers and {threads} threads', flush=True)
|
||||||
gunicorn_app.run()
|
gunicorn_app.run()
|
||||||
|
109
server.py
109
server.py
@ -24,6 +24,9 @@ from solana.rpc.types import TokenAccountOpts
|
|||||||
from pycoingecko import CoinGeckoAPI
|
from pycoingecko import CoinGeckoAPI
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
from cachetools import cached
|
from cachetools import cached
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import cache
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@ -31,8 +34,10 @@ app = Flask(__name__)
|
|||||||
|
|
||||||
solana_client = Client(os.getenv("SOLANA_URL"))
|
solana_client = Client(os.getenv("SOLANA_URL"))
|
||||||
blockFrost_API = os.getenv("BLOCKFROST")
|
blockFrost_API = os.getenv("BLOCKFROST")
|
||||||
stWDBRN_token_mint = Pubkey.from_string("mNT61ixgiLnggJ4qf5hDCNj7vTiCqnqysosjncrBydf")
|
stWDBRN_token_mint = Pubkey.from_string(
|
||||||
vault_sol_address = Pubkey.from_string("NWywvhcqdkJsm1s9VVviPm9UfyDtyCW9t8kDb24PDPN")
|
"mNT61ixgiLnggJ4qf5hDCNj7vTiCqnqysosjncrBydf")
|
||||||
|
vault_sol_address = Pubkey.from_string(
|
||||||
|
"NWywvhcqdkJsm1s9VVviPm9UfyDtyCW9t8kDb24PDPN")
|
||||||
vault_cardano_address = "stake1uy4qd785pcds7ph2jue2lrhhxa698c5959375lqdv3yphcgwc8qna"
|
vault_cardano_address = "stake1uy4qd785pcds7ph2jue2lrhhxa698c5959375lqdv3yphcgwc8qna"
|
||||||
|
|
||||||
fiat = "USD"
|
fiat = "USD"
|
||||||
@ -48,6 +53,8 @@ def find(name, path):
|
|||||||
return os.path.join(root, name)
|
return os.path.join(root, name)
|
||||||
|
|
||||||
# Assets routes
|
# Assets routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/assets/<path:path>")
|
@app.route("/assets/<path:path>")
|
||||||
def send_assets(path):
|
def send_assets(path):
|
||||||
if path.endswith(".json"):
|
if path.endswith(".json"):
|
||||||
@ -102,27 +109,24 @@ def index():
|
|||||||
vaultBalance = getVaultBalance()
|
vaultBalance = getVaultBalance()
|
||||||
vaultBalance = "{:.2f}".format(vaultBalance)
|
vaultBalance = "{:.2f}".format(vaultBalance)
|
||||||
|
|
||||||
|
|
||||||
# For testing
|
# For testing
|
||||||
# tokenSupply = 20
|
# tokenSupply = 20
|
||||||
# tokenValue = 1.01
|
# tokenValue = 1.01
|
||||||
# tokens = [{"symbol":"stWDBRN","name":"Stake With Us","value":1.01}]
|
# tokens = [{"symbol":"stWDBRN","name":"Stake With Us","value":1.01}]
|
||||||
# solValue = 10
|
# solValue = 10
|
||||||
# vaultBalance = "20.00"
|
# vaultBalance = "20.00"
|
||||||
|
|
||||||
|
|
||||||
pie_chart_data = [(token['symbol'].upper(), token['value'], f"{token['name']}: ${'{:.2f}'.format(token['value'])}") for token in tokens]
|
|
||||||
pie_chart_data.append(("SOL", solValue, f"Solana: ${'{:.2f}'.format(solValue)}"))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pie_chart_data = [(token['symbol'].upper(), token['value'], f"{token['name']}: ${
|
||||||
|
'{:.2f}'.format(token['value'])}") for token in tokens]
|
||||||
|
pie_chart_data.append(
|
||||||
|
("SOL", solValue, f"Solana: ${'{:.2f}'.format(solValue)}"))
|
||||||
|
|
||||||
cardanoBalance = getCardanoValue(vault_cardano_address)
|
cardanoBalance = getCardanoValue(vault_cardano_address)
|
||||||
cardanoBalance = "{:.2f}".format(cardanoBalance)
|
cardanoBalance = "{:.2f}".format(cardanoBalance)
|
||||||
pie_chart_data.append(("ADA", cardanoBalance, f"Cardano: ${cardanoBalance}"))
|
pie_chart_data.append(
|
||||||
|
("ADA", cardanoBalance, f"Cardano: ${cardanoBalance}"))
|
||||||
|
|
||||||
return render_template("index.html", value=tokenValue, supply=tokenSupply, pie_chart=pie_chart_data,vault=vaultBalance)
|
return render_template("index.html", value=tokenValue, supply=tokenSupply, pie_chart=pie_chart_data, vault=vaultBalance)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@ -150,52 +154,46 @@ def catch_all(path: str):
|
|||||||
|
|
||||||
|
|
||||||
# region Solana
|
# region Solana
|
||||||
|
@cache.file_cache()
|
||||||
coin_price_cache = TTLCache(maxsize=100, ttl=3600)
|
|
||||||
@cached(coin_price_cache)
|
|
||||||
def get_coin_price(coin_id):
|
def get_coin_price(coin_id):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if coin_id in stablecoins:
|
if coin_id in stablecoins:
|
||||||
return 1 * usd_to_fiat
|
return 1 * usd_to_fiat
|
||||||
|
|
||||||
price = coingecko_client.get_price(ids=coin_id, vs_currencies=fiat)
|
price = coingecko_client.get_price(ids=coin_id, vs_currencies=fiat)
|
||||||
|
|
||||||
return price[coin_id][fiat.lower()]
|
return price[coin_id][fiat.lower()]
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
token_price_cache = TTLCache(maxsize=100, ttl=3600)
|
@cache.file_cache()
|
||||||
@cached(token_price_cache)
|
def get_token_price(token_address: str):
|
||||||
def get_token_price(token_address:str):
|
|
||||||
try:
|
try:
|
||||||
price = coingecko_client.get_token_price(id='solana',contract_addresses=token_address,vs_currencies=fiat)
|
price = coingecko_client.get_token_price(
|
||||||
|
id='solana', contract_addresses=token_address, vs_currencies=fiat)
|
||||||
return price[token_address][fiat.lower()]
|
return price[token_address][fiat.lower()]
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@cache.file_cache()
|
||||||
token_supply_str_cache = TTLCache(maxsize=1, ttl=3600)
|
|
||||||
@cached(token_supply_str_cache)
|
|
||||||
def getTokenSupplyString() -> str:
|
def getTokenSupplyString() -> str:
|
||||||
supply = solana_client.get_token_supply(stWDBRN_token_mint)
|
supply = solana_client.get_token_supply(stWDBRN_token_mint)
|
||||||
return supply.value.ui_amount_string
|
return supply.value.ui_amount_string
|
||||||
|
|
||||||
token_supply_cache = TTLCache(maxsize=1, ttl=3600)
|
@cache.file_cache()
|
||||||
@cached(token_supply_cache)
|
|
||||||
def getTokenSupply() -> int:
|
def getTokenSupply() -> int:
|
||||||
supply = solana_client.get_token_supply(stWDBRN_token_mint)
|
supply = solana_client.get_token_supply(stWDBRN_token_mint)
|
||||||
return supply.value.ui_amount
|
return supply.value.ui_amount
|
||||||
|
|
||||||
sol_value_cache = TTLCache(maxsize=1, ttl=3600)
|
@cache.file_cache()
|
||||||
@cached(sol_value_cache)
|
|
||||||
def getSolValue() -> int:
|
def getSolValue() -> int:
|
||||||
SOLbalance = solana_client.get_balance(vault_sol_address).value / 1000000000
|
SOLbalance = solana_client.get_balance(
|
||||||
|
vault_sol_address).value / 1000000000
|
||||||
SOLPrice = get_coin_price("solana")
|
SOLPrice = get_coin_price("solana")
|
||||||
return SOLbalance * SOLPrice
|
return SOLbalance * SOLPrice
|
||||||
|
|
||||||
vault_balance_cache = TTLCache(maxsize=1, ttl=3600)
|
@cache.file_cache()
|
||||||
@cached(vault_balance_cache)
|
|
||||||
def getVaultBalance() -> int:
|
def getVaultBalance() -> int:
|
||||||
# Get balance of vault
|
# Get balance of vault
|
||||||
vaultBalance = 0
|
vaultBalance = 0
|
||||||
@ -210,16 +208,15 @@ def getVaultBalance() -> int:
|
|||||||
|
|
||||||
return vaultBalance
|
return vaultBalance
|
||||||
|
|
||||||
|
@cache.file_cache()
|
||||||
get_tokens_cache = TTLCache(maxsize=1, ttl=3600)
|
|
||||||
@cached(get_tokens_cache)
|
|
||||||
def getTokens():
|
def getTokens():
|
||||||
programID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
programID = Pubkey.from_string(
|
||||||
|
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
|
||||||
tokenAccounts = solana_client.get_token_accounts_by_owner(
|
tokenAccounts = solana_client.get_token_accounts_by_owner(
|
||||||
vault_sol_address,
|
vault_sol_address,
|
||||||
TokenAccountOpts(program_id=programID)
|
TokenAccountOpts(program_id=programID)
|
||||||
)
|
)
|
||||||
|
|
||||||
tokenAccounts = tokenAccounts.value
|
tokenAccounts = tokenAccounts.value
|
||||||
tokens = []
|
tokens = []
|
||||||
@ -237,7 +234,7 @@ def getTokens():
|
|||||||
|
|
||||||
token["price"] = get_token_price(token["mint"])
|
token["price"] = get_token_price(token["mint"])
|
||||||
token["value"] = token["price"] * token["balance"]
|
token["value"] = token["price"] * token["balance"]
|
||||||
|
|
||||||
if token["value"] < 0.01:
|
if token["value"] < 0.01:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -257,7 +254,8 @@ def getTokenData(tokenMint):
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
data = coingecko_client.get_coin_info_from_contract_address_by_id('solana', tokenMint)
|
data = coingecko_client.get_coin_info_from_contract_address_by_id(
|
||||||
|
'solana', tokenMint)
|
||||||
with open(f"tokens/{tokenMint}.json", "w") as f:
|
with open(f"tokens/{tokenMint}.json", "w") as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
return data
|
return data
|
||||||
@ -270,7 +268,7 @@ def getTokenPrice():
|
|||||||
value = vaultBalance/supply
|
value = vaultBalance/supply
|
||||||
# Round value to 2 decimal places and ensure it shows 2 decimal places
|
# Round value to 2 decimal places and ensure it shows 2 decimal places
|
||||||
value = "{:.2f}".format(value)
|
value = "{:.2f}".format(value)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@ -278,11 +276,14 @@ def getTokenPrice():
|
|||||||
|
|
||||||
# region Cardano
|
# region Cardano
|
||||||
get_cardano_balance_cache = TTLCache(maxsize=1, ttl=3600)
|
get_cardano_balance_cache = TTLCache(maxsize=1, ttl=3600)
|
||||||
@cached(get_cardano_balance_cache)
|
|
||||||
|
|
||||||
|
@cache.file_cache()
|
||||||
def getCardanoBalance(address: str):
|
def getCardanoBalance(address: str):
|
||||||
# Get balance of cardano address
|
# Get balance of cardano address
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"https://cardano-mainnet.blockfrost.io/api/v0/accounts/{address}",headers={"project_id": blockFrost_API})
|
response = requests.get(f"https://cardano-mainnet.blockfrost.io/api/v0/accounts/{
|
||||||
|
address}", headers={"project_id": blockFrost_API})
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print("Error getting cardano balance")
|
print("Error getting cardano balance")
|
||||||
return 0
|
return 0
|
||||||
@ -295,6 +296,7 @@ def getCardanoBalance(address: str):
|
|||||||
print("Error getting cardano balance")
|
print("Error getting cardano balance")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def getCardanoValue(address: str):
|
def getCardanoValue(address: str):
|
||||||
balance = getCardanoBalance(address)
|
balance = getCardanoBalance(address)
|
||||||
price = get_coin_price("cardano")
|
price = get_coin_price("cardano")
|
||||||
@ -304,11 +306,32 @@ def getCardanoValue(address: str):
|
|||||||
|
|
||||||
# region Error Catching
|
# region Error Catching
|
||||||
# 404 catch all
|
# 404 catch all
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
# region Background Threads
|
||||||
|
def update_data():
|
||||||
|
try:
|
||||||
|
print("Updating Solana data...")
|
||||||
|
getSolValue()
|
||||||
|
getTokens()
|
||||||
|
getVaultBalance()
|
||||||
|
print("Updating Cardano data...")
|
||||||
|
getCardanoBalance(vault_cardano_address)
|
||||||
|
getCardanoValue(vault_cardano_address)
|
||||||
|
print("Updating data complete.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating data: {e}")
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
threading.Thread(target=update_data).start()
|
||||||
app.run(debug=True, port=5000, host="0.0.0.0")
|
app.run(debug=True, port=5000, host="0.0.0.0")
|
||||||
|
Loading…
Reference in New Issue
Block a user