From 49c73f55af9c72e6f901ccea6ad308789cbcf050 Mon Sep 17 00:00:00 2001 From: Jimmy Chu <898091+jimmychu0807@users.noreply.github.com> Date: Sat, 28 Sep 2024 06:35:58 +0800 Subject: [PATCH] Added test cases (#13) * output to csv and json * added gitignore file * Written test files * Run CI action with secrets --- .github/workflows/ci.yml | 2 +- .gitignore | 3 ++ pdm.lock | 4 +- pyproject.toml | 8 ++-- src/scripts/__init__.py | 5 ++- src/scripts/airdrop.py | 28 +++++++++----- src/scripts/main.py | 12 +++--- src/scripts/utils.py | 4 ++ tests/scripts/test_airdrop.py | 73 +++++++++++++++++++++++++++++++++-- 9 files changed, 113 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9d4aca..63de2dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,4 +25,4 @@ jobs: pdm sync -d - name: Run Tests run: | - pdm run all + CRYPTOCOMPARE_APIKEY=${{secrets.CRYPTOCOMPARE_APIKEY}} pdm run all diff --git a/.gitignore b/.gitignore index 82f9275..43756cb 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# project specific file +tests/fixtures/config.json diff --git a/pdm.lock b/pdm.lock index 1a8100c..afd3f22 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "lint", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:36015079be128d84a51e3133899930b732eb23035d2416d6435927abb6940946" +content_hash = "sha256:301b37b88d4f5c3243499bbd3162c99e29b8dc74368c63aa0a4c66675af1bedc" [[metadata.targets]] requires_python = ">=3.10" @@ -462,7 +462,7 @@ name = "python-dotenv" version = "1.0.1" requires_python = ">=3.8" summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["default"] +groups = ["default", "test"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, diff --git a/pyproject.toml b/pyproject.toml index c8c0ee2..be16271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["pdm-backend"] build-backend = "pdm.backend" [project] -name = "airdrop-scripts" +name = "dhkdao_airdrop_scripts" version = "0.1.0" authors = [ { name = "Jimmy Chu", email = "jimmychu0807@gmail.com" }, @@ -28,6 +28,7 @@ lint = [ ] test = [ "pytest>=8.3.3", + "python-dotenv>=1.0.1" ] [project.urls] @@ -38,8 +39,9 @@ Issues = "https://github.com/dhkdao/airdrop-scripts/issues" distribution = true [tool.pdm.scripts] -test = "pytest tests" +test = "pytest --capture=no tests" +"test:ci" = "pytest" lint = "flake8 src tests" "lint:write" = "black src tests" exe = "pdm run src/scripts/main.py" -all = { composite = ["lint", "test"]} +all = { composite = ["lint", "test:ci"]} diff --git a/src/scripts/__init__.py b/src/scripts/__init__.py index 989cdea..5e759a9 100644 --- a/src/scripts/__init__.py +++ b/src/scripts/__init__.py @@ -1,3 +1,4 @@ -from .airdrop import airdrop_monthly_alloc +from .airdrop import Airdrop +from .utils import is_number, round_output -__all__ = ["airdrop_monthly_alloc"] +__all__ = ["Airdrop", "is_number", "round_output"] diff --git a/src/scripts/airdrop.py b/src/scripts/airdrop.py index 496266d..ab8a3a3 100644 --- a/src/scripts/airdrop.py +++ b/src/scripts/airdrop.py @@ -8,6 +8,8 @@ from rich import print from typing import Optional +from scripts.utils import is_number + class Airdrop: @classmethod @@ -24,8 +26,7 @@ def check_config(cls, config): return True - def __init__(self, config_file): - config = json.loads(config_file.read()) + def __init__(self, config): if Airdrop.check_config(config): self.config = config @@ -127,17 +128,22 @@ def monthly_alloc(self): token, staking_amt = t["token"], t["qty"] token_price = t["price"] if "price" in t else self.fetch_price(token) - staking_val = token_price * staking_amt - staking_apr = 0 - if "staking_apr" in t: - staking_apr = t["staking_apr"] + staking_val = ( + token_price * staking_amt + if is_number(token_price) and is_number(staking_amt) + else None + ) + + staking_apr = None + if "staking-apr" in t: + staking_apr = t["staking-apr"] elif "network" in t: staking_apr = self.fetch_staking_apr(t["network"]) # NX> handles TypeError: unsupported operand type(s) for *: 'NoneType' and 'float' reward = ( staking_apr * staking_val - if isinstance(staking_apr, float) and isinstance(staking_val, float) + if is_number(staking_apr) and is_number(staking_val) else None ) @@ -167,9 +173,13 @@ def monthly_alloc(self): # Calculate DHK distribution and distribution percent for idx, row in df.iterrows(): - df.at[idx, "dhk-distribution-pc"] = (row["reward"] / ttl_reward) * 100 + df.at[idx, "dhk-distribution-pc"] = ( + (row["reward"] / ttl_reward) * 100 if is_number(row["reward"]) else None + ) df.at[idx, "dhk-distribution"] = ( - row["reward"] / ttl_reward * self.config["dhk_distribution"] + (row["reward"] / ttl_reward) * self.config["dhk_distribution"] + if is_number(row["reward"]) + else None ) return df diff --git a/src/scripts/main.py b/src/scripts/main.py index 6554d81..9e59bfb 100644 --- a/src/scripts/main.py +++ b/src/scripts/main.py @@ -1,10 +1,10 @@ +import json import typer from typing_extensions import Annotated from enum import Enum from rich import print -import utils -from airdrop import Airdrop +from scripts import round_output, Airdrop class OutputType(str, Enum): @@ -34,16 +34,16 @@ def monthly_alloc( """ Compute the DHK dao monthly airdrop allocation based on staked value on various blockchains. """ - airdrop = Airdrop(config) - result = utils.round_output(airdrop.monthly_alloc()) + airdrop = Airdrop(json.loads(config.read())) + result = round_output(airdrop.monthly_alloc()) result_output = None # Transform the result suitable for output match type: case OutputType.json: - pass + result_output = result.to_json(orient="records") case OutputType.csv: - pass + result_output = result.to_csv() case OutputType.table: result_output = result case _: # Unexpected output type diff --git a/src/scripts/utils.py b/src/scripts/utils.py index f142916..fc6aab3 100644 --- a/src/scripts/utils.py +++ b/src/scripts/utils.py @@ -13,3 +13,7 @@ def round_output(df): for label, col_data in df.items(): df[label] = col_data.apply(round_number) return df + + +def is_number(i): + return isinstance(i, (int, float)) diff --git a/tests/scripts/test_airdrop.py b/tests/scripts/test_airdrop.py index d0b9554..79a93b2 100644 --- a/tests/scripts/test_airdrop.py +++ b/tests/scripts/test_airdrop.py @@ -1,3 +1,70 @@ -class TestClass: - def test(self): - assert True +import json +import os +import pytest +import typer +from dotenv import load_dotenv +from scripts import Airdrop + + +load_dotenv() + +UNKNOWN_TOKEN = "HASH" +KNOWN_TOKEN = "ATOM" +INVALID_CONFIG_FILEPATH = os.path.join( + os.path.dirname(__file__), "../fixtures/config-invalid-1.json" +) + +print("INVALID_CONFIG_FILEPATH", INVALID_CONFIG_FILEPATH) + + +def get_config(): + return json.loads( + """{ + "dhk_distribution": 100000, + "reference_date": "2024-08-27", + "tokens": [ + { "token": "AKT", "network": "akash", "qty": 188787 }, + { "token": "ATOM", "network": "cosmos", "qty": 592015 } + ], + "apis": { + "cryptocompare": { + "endpoint": "https://min-api.cryptocompare.com/data/pricehistorical" + }, + "mintscan": { + "endpoint": "https://apis.mintscan.io/v1/:network/apr" + } + } + }""" + ) + + +def get_config_with_cryptocompare_apikey_env(): + config = get_config() + config["apis"]["cryptocompare"]["apikey"] = os.getenv("CRYPTOCOMPARE_APIKEY") + return config + + +class TestAirdropClass: + def test_fetch_price_on_unknown_token_should_return_none(self): + config = get_config_with_cryptocompare_apikey_env() + airdrop = Airdrop(config) + price = airdrop.fetch_price(UNKNOWN_TOKEN) + assert price is None + + def test_fetch_price_on_known_token_should_return_value(self): + config = get_config_with_cryptocompare_apikey_env() + airdrop = Airdrop(config) + price = airdrop.fetch_price(KNOWN_TOKEN) + assert isinstance(price, float) + + def test_on_config_missing_key_should_raise_exception(self): + with open(INVALID_CONFIG_FILEPATH, "r") as f: + config = json.loads(f.read()) + + with pytest.raises(typer.Exit) as excinfo: + Airdrop(config) + + assert excinfo.type is typer.Exit + + def test_fetch_staking_apr(self): + pass