diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..30cf777 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +source = + src/ + tests/ + +branch = True +omit = + venv/ + +[report] +exclude_lines = + pragma: no cover + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index bed1671..fb0170d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /venv/ __pycache__ -history.log \ No newline at end of file +history.log + +.coverage \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index edbe260..52de093 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,19 @@ -FROM python:3.7 +FROM python:3.7.11-slim WORKDIR /app -RUN apt-get update && apt-get install -y python-pip python3.7 python3-setuptools autoconf libtool pkg-config python3-dev build-essential +RUN apt-get update && apt-get install COPY ./requirements.txt /app/requirements.txt +RUN pip install -r /app/requirements.txt -COPY . /app +COPY ./src/exchange.py /app/exchange.py -# ENV AUCTION_DEBUG_MODE False -ENV FLASK_RUN_HOST=0.0.0.0 +#COPY . /app -EXPOSE 2222 +# ENV AUCTION_DEBUG_MODE False +# ENV FLASK_RUN_HOST=0.0.0.0 -WORKDIR /app +#EXPOSE 2222 -RUN pip install -r requirements.txt \ No newline at end of file +CMD ["gunicorn", "--bind 0.0.0.0:2222", "--timeout=600", "exchange:app", "-w 1", "--threads 5"] diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 1147d91..d604908 --- a/build.sh +++ b/build.sh @@ -1,3 +1,3 @@ #!/bin/bash -app="oracle-image" -docker build -t ${app} . +app="crypto-oracle" +docker build -t ${app}:$(git describe --always --tags --dirty --abbrev=7) . diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..bf201cb --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,5 @@ +version: "3.7" +services: + crypto-oracle: + ports: + - "2222:2222" diff --git a/docker-compose.yml b/docker-compose.yml index c8a3fb5..43deb87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,17 @@ -version: "3" +version: "3.7" services: - flask: - container_name: oracle - image: oracle-image - ports: - - 0.0.0.0:2222:2222 + crypto-oracle: + image: registry.digitalocean.com/biggestfan/crypto/crypto-oracle:${TAG_CRYPTO_ORACLE:-latest} + build: + context: ${CRYPTO_ORACLE_DIR:-./} + dockerfile: ./Dockerfile volumes: - ./data/:/app/data/ - hostname: "bitcoin-bridging" + networks: + - bitcoin-bridging command: gunicorn --bind 0.0.0.0:2222 --timeout=600 exchange:app -w 1 --threads 5 - networks: - default: - external: - name: bitcoin-bridging + bitcoin-bridging: + external: true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 08026fe..7e6a134 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ flask==1.1.2 flask_restful==0.3.8 gunicorn==20.0.4 gevent==21.1.2 -requests==2.25.1 \ No newline at end of file +requests==2.25.1 +pytest==6.2.5 +coverage==5.5 diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..77e2911 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,17 @@ +import os # pragma: no cover +import subprocess # pragma: no cover + + +def main(): # pragma: no cover + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + subprocess.call(["coverage", "run", "-m", "pytest", "-rw"]) + print("\n\nTests completed, checking coverage...\n\n") + + subprocess.call(["coverage", "combine", "--append"]) + subprocess.call(["coverage", "report", "-m"]) + input("\n\nPress enter to quit ") + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/exchange.py b/src/exchange.py similarity index 52% rename from exchange.py rename to src/exchange.py index 5cd59ae..9bf407f 100644 --- a/exchange.py +++ b/src/exchange.py @@ -9,6 +9,7 @@ from flask import Flask from flask_restful import Resource, Api +from decimal import Decimal logging.basicConfig(filename='history.log', filemode='w', level=logging.DEBUG) @@ -19,23 +20,24 @@ last_rate = {} # mutex = threading.Lock() bitkub_url = "https://api.bitkub.com/api/market/ticker?sym=THB_BCH" +usd_exchange_rate_url = "https://cex.io/api/ticker/BCH/USD" NEW_RESULT_INTERVAL = 10 # Seconds class Rates: # Stores current rates out of global scope def __init__(self): - self.bitkub_rates = {} + self.rates = {} - def get_rate(self): - return self.bitkub_rates + def get_rate(self, denomination): + return self.rates.get(denomination, None) - def add_rate(self, info): - self.bitkub_rates = info + def add_rate(self, denomination, info): + self.rates[denomination] = info # rates class gets passed so thread has access -def start_rate_thread(rates): +def start_rate_thread(rates): # pragma: no cover logging.debug("starting") rate_thread = threading.Thread(target=rate_fetcher, args=(rates,)) rate_thread.daemon = True @@ -43,15 +45,38 @@ def start_rate_thread(rates): return # rates class gets passed so thread has access -def rate_fetcher(rates): +def rate_fetcher(rates): # pragma: no cover while True: rates = rates try: # get current exchange rate from bitkub - result = requests.get(bitkub_url) + thb_result = requests.get(bitkub_url) + usd_result = requests.get(usd_exchange_rate_url) logging.debug("grabbing new rate:") - logging.debug(f"result: {result.json()}") - rates.add_rate(result.json()) + logging.debug(f"result: {thb_result.json()}") + logging.debug(f"result: {usd_result.json()}") + + thb_json = thb_result.json() + usd_json = usd_result.json() + + thb_rate = { + "last": int(Decimal(str(thb_json["THB_BCH"]["last"])) * 100), + "multiplier": 100, + "raw": thb_json, + "from": bitkub_url, + "timestamp": int(time.time()) + } + + usd_rate = { + "last": int(Decimal(str(usd_json["last"])) * 100), + "multiplier": 100, + "raw": usd_json, + "from": usd_exchange_rate_url, + "timestamp": int(usd_json["timestamp"]) + } + + rates.add_rate("THB", thb_rate) + rates.add_rate("USD", usd_rate) except Exception as err: logging.debug(f"An error occurred: {err}") pass @@ -64,10 +89,19 @@ class GetRate(Resource): def __init__(self, **kwargs): self.rates = kwargs["rates"] - def get(self, amount, denomination): + def get(self, denomination): # Format in docs - return {"thb-bch": self.rates.get_rate()} - + rate = self.rates.get_rate(denomination) + if not rate: + return {"message": "This denomination is not available"}, 404 + + result = { + "last": rate["last"], + "multiplier": rate["multiplier"], + "timestamp": rate["timestamp"] + } + + return result, 200 class Exchange(Flask): # pragma: no cover @@ -99,7 +133,7 @@ def run( app = Exchange(__name__) api = Api(app) -api.add_resource(GetRate, '/api/get_rate//', resource_class_kwargs={ +api.add_resource(GetRate, '/api/get_rate/', resource_class_kwargs={ "rates": rates}) if __name__ == '__main__': diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/tests/samples.py b/tests/samples.py new file mode 100644 index 0000000..8784840 --- /dev/null +++ b/tests/samples.py @@ -0,0 +1,2 @@ +thb_rate_json = {"THB_BCH":{"id":6,"last":17069.79,"lowestAsk":17062.1,"highestBid":17039.27,"percentChange":2.31,"baseVolume":542.12365819,"quoteVolume":9338020.59,"isFrozen":0,"high24hr":17630.96,"low24hr":16586.02,"change":385.57,"prevClose":17069.79,"prevOpen":16684.22}} + diff --git a/tests/test_exchange.py b/tests/test_exchange.py new file mode 100644 index 0000000..842b33c --- /dev/null +++ b/tests/test_exchange.py @@ -0,0 +1,79 @@ +import pytest +import json +import unittest +from unittest import mock +from unittest.mock import mock_open + + + +from src.exchange import ( + Rates, + bitkub_url, + usd_exchange_rate_url, + app, + rates +) + +from tests.samples import ( + thb_rate_json, +) + + + + +class Tests: + def test_get_rate(self): + rates = Rates() + + thb_rate = { + "last": thb_rate_json["THB_BCH"]["last"], + "raw": thb_rate_json, + "from": bitkub_url + } + rates.add_rate("thb", thb_rate) + + assert rates.get_rate("thb") == thb_rate + + + def test_denomination_not_found(self): + rates = Rates() + + assert rates.get_rate("not_found") == None + + + def test_denomination_not_available(self): + + # denomination = "thb" + url = f"/api/get_rate/THB" + + flask_app = app + with flask_app.test_client(self) as test_client: + + response = test_client.get(url, content_type="application/json") + result = json.loads(response.get_data(as_text=True)) + + assert response.status_code == 404 + assert result == {'message': 'This denomination is not available'} + + def test_get_thb(self): + + # denomination = "thb" + url = f"/api/get_rate/THB" + + thb_rate = { + "last": 20000, + "multiplier": 100, + "timestamp": 2000000 + } + + rates.add_rate("THB", thb_rate) + + flask_app = app + with flask_app.test_client(self) as test_client: + + response = test_client.get(url, content_type="application/json") + result = json.loads(response.get_data(as_text=True)) + + assert response.status_code == 200 + assert result == thb_rate + \ No newline at end of file