From 72a165217c8c2bedb8ccc2dac4ecc835d6634c28 Mon Sep 17 00:00:00 2001 From: Paulo Antunes Date: Sun, 16 Jul 2023 18:02:23 +0200 Subject: [PATCH 1/3] Add Coinbase & Add UI Improvements --- .dockerignore | 4 + .envs/.local/.producer | 6 + .envs/.local/.web | 5 +- Makefile | 14 +- README.md | 3 +- compose/producer/coinbase/Dockerfile | 47 +++ compose/producer/coinbase/start | 7 + exchange_radar/__init__.py | 26 +- exchange_radar/producer/publisher.py | 3 +- exchange_radar/producer/schemas/base.py | 6 +- exchange_radar/producer/schemas/coinbase.py | 31 ++ exchange_radar/producer/settings/base.py | 2 +- exchange_radar/producer/settings/exchanges.py | 2 + exchange_radar/producer/task.py | 2 +- exchange_radar/producer/tasks/binance.py | 2 +- exchange_radar/producer/tasks/coinbase.py | 50 +++ exchange_radar/producer/tasks/kucoin.py | 2 +- exchange_radar/producer/utils.py | 1 + exchange_radar/tests/producer/test_schemas.py | 52 ++- exchange_radar/web/src/settings/base.py | 5 +- exchange_radar/web/src/urls.py | 1 + exchange_radar/web/src/utils.py | 14 + exchange_radar/web/src/views/http.py | 2 + exchange_radar/web/static/css/custom.css | 25 +- exchange_radar/web/static/css/exchanges.css | 14 + exchange_radar/web/static/css/style.css | 1 - exchange_radar/web/static/css/theme.css | 91 +++++ exchange_radar/web/templates/base.html | 78 ++++- .../web/templates/index-dolphins.html | 3 +- .../web/templates/index-octopuses.html | 3 +- .../web/templates/index-whales.html | 3 +- exchange_radar/web/templates/index.html | 3 +- local.yml | 15 + poetry.lock | 315 +++++++++++++++--- production.yml | 13 + pyproject.toml | 5 +- 36 files changed, 729 insertions(+), 127 deletions(-) create mode 100644 compose/producer/coinbase/Dockerfile create mode 100644 compose/producer/coinbase/start create mode 100644 exchange_radar/producer/schemas/coinbase.py create mode 100644 exchange_radar/producer/tasks/coinbase.py create mode 100644 exchange_radar/web/src/utils.py create mode 100644 exchange_radar/web/static/css/exchanges.css create mode 100644 exchange_radar/web/static/css/theme.css diff --git a/.dockerignore b/.dockerignore index 3920327..fbe4f23 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,10 @@ .idea .git venv +.venv nginx +.pytest_cache +htmlcov .gitignore .pre-commit-config.yaml +.coverage diff --git a/.envs/.local/.producer b/.envs/.local/.producer index 4da35f0..430192c 100644 --- a/.envs/.local/.producer +++ b/.envs/.local/.producer @@ -17,3 +17,9 @@ LINK-USDT FTM-USDT PEPE-USDT " + +COINBASE_COINS=" +BTC-USD +ETH-USD +LINK-USD +" diff --git a/.envs/.local/.web b/.envs/.local/.web index daf8a64..7f48d47 100644 --- a/.envs/.local/.web +++ b/.envs/.local/.web @@ -1,6 +1,7 @@ DEBUG=True DB_PATH=/app/exchange_radar/web/data/db.json +DB_TABLE_MAX_ROWS=64 TRADES_SOCKET_URL=ws://127.0.0.1:9000/trades/{coin} TRADES_WHALES_SOCKET_URL=ws://127.0.0.1:9000/trades/{coin}/whales @@ -12,4 +13,6 @@ TRADES_WHALES_HOST_URL=http://127.0.0.1:9000/feed/{coin}/whales TRADES_DOLPHINS_HOST_URL=http://127.0.0.1:9000/feed/{coin}/dolphins TRADES_OCTOPUSES_HOST_URL=http://127.0.0.1:9000/feed/{coin}/octopuses -DB_TABLE_MAX_ROWS=64 +BINANCE=LTO,ETH,BTC,LINK,FTM,PEPE +KUCOIN=LTO,ETH,BTC,LINK,FTM,PEPE +COINBASE=ETH,BTC,LINK diff --git a/Makefile b/Makefile index 586ec69..1942fe6 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ pre-commit: pre-commit-verify: @docker-compose -f local.yml run --rm tests bash -c \ - "git config --global --add safe.directory /app && \ - git init && \ - git add . && \ - pre-commit run --color=always --show-diff-on-failure --all-files" +"git config --global --add safe.directory /app && \ +git init && \ +git add . && \ +pre-commit run --color=always --show-diff-on-failure --all-files" tests: @docker-compose -f local.yml run --rm tests pytest @@ -19,7 +19,7 @@ tests-shell: coverage: @docker-compose -f local.yml run --rm tests bash -c \ - "coverage run -m pytest -v --tb=short -p no:warnings && \ - coverage report && \ - coverage html" +"coverage run -m pytest -v --tb=short -p no:warnings && \ +coverage report && \ +coverage html" open htmlcov/index.html diff --git a/README.md b/README.md index 7834734..2b6416d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # exchange-radar Get real-time trades from various crypto exchanges. -So far the following crypto exchanges are implemented: +So far the following crypto exchanges are supported: - Binance - Kucoin +- Coinbase ### Build & Run As simple as: diff --git a/compose/producer/coinbase/Dockerfile b/compose/producer/coinbase/Dockerfile new file mode 100644 index 0000000..74c4c0d --- /dev/null +++ b/compose/producer/coinbase/Dockerfile @@ -0,0 +1,47 @@ +ARG PYTHON_VERSION=3.11.3-slim-bullseye + +FROM python:${PYTHON_VERSION} as python + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + POETRY_HOME="/opt/poetry" \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 \ + PYSETUP_PATH="/app" \ + VENV_PATH="/app/.venv" + +ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" + + +FROM python as python-build-stage + +RUN apt-get update && apt-get install --no-install-recommends -y \ + curl \ + build-essential + +ENV POETRY_VERSION=1.5.1 +RUN curl -sSL https://install.python-poetry.org | python + +WORKDIR $PYSETUP_PATH + +COPY ./poetry.lock ./pyproject.toml ./ +RUN poetry install --only main --no-root + + +FROM python as python-run-stage + +COPY --from=python-build-stage $POETRY_HOME $POETRY_HOME +COPY --from=python-build-stage $PYSETUP_PATH $PYSETUP_PATH + +COPY ./compose/producer/coinbase/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +WORKDIR $PYSETUP_PATH + +COPY . . + +RUN poetry install diff --git a/compose/producer/coinbase/start b/compose/producer/coinbase/start new file mode 100644 index 0000000..86cbd4c --- /dev/null +++ b/compose/producer/coinbase/start @@ -0,0 +1,7 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +exec poetry run producer ${COINBASE_COINS} --exchange Coinbase diff --git a/exchange_radar/__init__.py b/exchange_radar/__init__.py index d0e4171..7f76a96 100644 --- a/exchange_radar/__init__.py +++ b/exchange_radar/__init__.py @@ -1,12 +1,14 @@ -__title__ = "exchange-radar" -__description__ = "Get real-time trades from various crypto exchanges" -__author__ = "Paulo Antunes" -__copyright__ = "Copyright 2023, exchange-radar" -__credits__ = [ - "Paulo Antunes", -] -__license__ = "GPLv3" -__maintainer__ = "Paulo Antunes" -__email__ = "pjmlantunes@gmail.com" - -__version__ = "0.1.0" +import tomllib + +with open("pyproject.toml", "rb") as f: + data = tomllib.load(f) + +toml = data["tool"]["poetry"] + + +__title__ = toml["name"] +__description__ = toml["description"] +__authors__ = toml["authors"] +__license__ = toml["license"] + +__version__ = toml["version"] diff --git a/exchange_radar/producer/publisher.py b/exchange_radar/producer/publisher.py index 6e736cd..1db497c 100644 --- a/exchange_radar/producer/publisher.py +++ b/exchange_radar/producer/publisher.py @@ -11,6 +11,7 @@ from exchange_radar.producer.enums import Ranking from exchange_radar.producer.schemas.binance import BinanceTradeSchema +from exchange_radar.producer.schemas.coinbase import CoinbaseTradeSchema from exchange_radar.producer.schemas.kucoin import KucoinTradeSchema from exchange_radar.producer.settings import base as settings from exchange_radar.producer.utils import get_ranking @@ -73,7 +74,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): producer_connection = ProducerConnection() -def publish(data: BinanceTradeSchema | KucoinTradeSchema) -> None: +def publish(data: BinanceTradeSchema | KucoinTradeSchema | CoinbaseTradeSchema) -> None: logger.info(f"PUB: {data.trade_time} {data.symbol}") try: diff --git a/exchange_radar/producer/schemas/base.py b/exchange_radar/producer/schemas/base.py index 1acb112..9b2ce79 100644 --- a/exchange_radar/producer/schemas/base.py +++ b/exchange_radar/producer/schemas/base.py @@ -38,10 +38,10 @@ def trade_time_after(cls, v) -> datetime: def message(self) -> str: return ( f"{self.trade_time} | " # noqa - f"{self.exchange.ljust(8, ' ')} | " # noqa - f"{'{:.8f} {}'.format(self.price, self.currency).rjust(14 + 5, ' ')} | " # noqa + f"{self.exchange.ljust(8, ' ')} | " # noqa + f"{'{:.8f} {}'.format(self.price, self.currency.rjust(4)).rjust(14 + 5, ' ')} | " # noqa f"{'{:.8f} {}'.format(self.quantity, self.trade_symbol).rjust(21 + 5, ' ')} | " # noqa - f"{'{:.8f} {}'.format(self.total, self.currency).rjust(17 + 5, ' ')}" # noqa + f"{'{:.8f} {}'.format(self.total, self.currency.rjust(4)).rjust(17 + 5, ' ')}" # noqa ) @computed_field diff --git a/exchange_radar/producer/schemas/coinbase.py b/exchange_radar/producer/schemas/coinbase.py new file mode 100644 index 0000000..4d8c736 --- /dev/null +++ b/exchange_radar/producer/schemas/coinbase.py @@ -0,0 +1,31 @@ +from datetime import datetime +from functools import cached_property + +from pydantic import Field, computed_field, condecimal, field_validator + +from exchange_radar.producer.schemas.base import CustomBaseModel + + +class CoinbaseTradeSchema(CustomBaseModel): + symbol: str = Field(alias="product_id") + price: condecimal(ge=0, decimal_places=8) + quantity: condecimal(ge=0, decimal_places=8) = Field(alias="size") + trade_time: datetime = Field(alias="time") + side: str = Field(exclude=True) + + @field_validator("symbol") + def symbol_normalization(cls, v) -> str: + return v.replace("-", "") + + @field_validator("trade_time") + def trade_time_normalization(cls, v) -> str: + return v.replace(tzinfo=None) + + @computed_field + @cached_property + def is_seller(self) -> bool: + return True if self.side == "sell" else False + + @computed_field + def exchange(self) -> str: + return "Coinbase" diff --git a/exchange_radar/producer/settings/base.py b/exchange_radar/producer/settings/base.py index 0a90557..cf822b3 100644 --- a/exchange_radar/producer/settings/base.py +++ b/exchange_radar/producer/settings/base.py @@ -19,4 +19,4 @@ RABBITMQ_CONNECTION_HEARTBEAT = env.int("RABBITMQ_CONNECTION_HEARTBEAT") RABBITMQ_BLOCKED_CONNECTION_TIMEOUT = env.int("RABBITMQ_BLOCKED_CONNECTION_TIMEOUT") -CURRENCIES = ["USDT", "BTC", "ETH", "BUSD"] +CURRENCIES = ["USDT", "BTC", "ETH", "BUSD", "USD"] diff --git a/exchange_radar/producer/settings/exchanges.py b/exchange_radar/producer/settings/exchanges.py index 41def61..bccdaae 100644 --- a/exchange_radar/producer/settings/exchanges.py +++ b/exchange_radar/producer/settings/exchanges.py @@ -1,7 +1,9 @@ from exchange_radar.producer.tasks.binance import BinanceTradesTask +from exchange_radar.producer.tasks.coinbase import CoinbaseTradesTask from exchange_radar.producer.tasks.kucoin import KucoinTradesTask EXCHANGES = { "Binance": BinanceTradesTask, "Kucoin": KucoinTradesTask, + "Coinbase": CoinbaseTradesTask, } diff --git a/exchange_radar/producer/task.py b/exchange_radar/producer/task.py index 2445830..2826071 100644 --- a/exchange_radar/producer/task.py +++ b/exchange_radar/producer/task.py @@ -12,7 +12,7 @@ def __init__(self): async def task(self, symbols: tuple[str]): await asyncio.gather(*[self.process(s) for s in symbols]) - async def process(self, symbol_or_symbols: str): + async def process(self, symbol_or_symbols: str | tuple): raise NotImplementedError("Task to be Processed") def start(self, symbols: tuple[str, ...]): diff --git a/exchange_radar/producer/tasks/binance.py b/exchange_radar/producer/tasks/binance.py index 2422c69..747136e 100644 --- a/exchange_radar/producer/tasks/binance.py +++ b/exchange_radar/producer/tasks/binance.py @@ -14,7 +14,7 @@ class BinanceTradesTask(Task): - async def process(self, symbol_or_symbols: str): + async def process(self, symbol_or_symbols: str | tuple): while True: try: async_client = await AsyncClient.create() diff --git a/exchange_radar/producer/tasks/coinbase.py b/exchange_radar/producer/tasks/coinbase.py new file mode 100644 index 0000000..e04de3f --- /dev/null +++ b/exchange_radar/producer/tasks/coinbase.py @@ -0,0 +1,50 @@ +import logging +import time + +from copra.websocket import Channel, Client + +from exchange_radar.producer.publisher import publish +from exchange_radar.producer.schemas.coinbase import CoinbaseTradeSchema +from exchange_radar.producer.task import Task + +logger = logging.getLogger(__name__) + + +ITER_SLEEP = 10 * 60.0 + + +class CoinbaseTradesTask(Task): + async_client = None + + def process(self, symbol_or_symbols: str | tuple): + class CustomClient(Client): + def on_message(self, message): + try: + data = CoinbaseTradeSchema(**message) + publish(data) + except Exception as _error: + logger.error(f"ERROR: {_error}") + + while True: + try: + self.async_client = CustomClient( + self.loop, Channel("matches", list(symbol_or_symbols)) + ) + break + except Exception as error: + logger.error(f"ERROR: {error}") + logger.error(f"Trying again in {ITER_SLEEP} seconds...") + time.sleep(ITER_SLEEP) + + def start(self, symbols: tuple[str, ...]): + logger.info("Starting Task...") + try: + self.process(symbol_or_symbols=symbols) + self.loop.run_forever() + except KeyboardInterrupt: + if self.async_client is not None: + self.loop.run_until_complete(self.async_client.close()) + self.loop.close() + logger.info("Task was interrupted...") + finally: + pass diff --git a/exchange_radar/producer/tasks/kucoin.py b/exchange_radar/producer/tasks/kucoin.py index eaf2c6d..36d8fb8 100644 --- a/exchange_radar/producer/tasks/kucoin.py +++ b/exchange_radar/producer/tasks/kucoin.py @@ -29,7 +29,7 @@ async def _subscribe( async def task(self, symbols: tuple[str]): await asyncio.gather(self.process(",".join(symbols))) - async def process(self, symbol_or_symbols: str): + async def process(self, symbol_or_symbols: str | tuple): try: async def callback(res): diff --git a/exchange_radar/producer/utils.py b/exchange_radar/producer/utils.py index b484eb8..9245635 100644 --- a/exchange_radar/producer/utils.py +++ b/exchange_radar/producer/utils.py @@ -7,6 +7,7 @@ def get_ranking(data: BinanceTradeSchema | KucoinTradeSchema) -> Ranking | None: if data.currency in ( "USDT", "BUSD", + "USD", ): if data.total > 100000.0: return Ranking.WHALE diff --git a/exchange_radar/tests/producer/test_schemas.py b/exchange_radar/tests/producer/test_schemas.py index 8021ffb..61aeea5 100644 --- a/exchange_radar/tests/producer/test_schemas.py +++ b/exchange_radar/tests/producer/test_schemas.py @@ -2,6 +2,7 @@ from decimal import Decimal from exchange_radar.producer.schemas.binance import BinanceTradeSchema +from exchange_radar.producer.schemas.coinbase import CoinbaseTradeSchema from exchange_radar.producer.schemas.kucoin import KucoinTradeSchema @@ -31,11 +32,11 @@ def test_schemas_binance(): "total": Decimal("0.100"), "currency": "BTC", "trade_symbol": "BNB", + "message": "2022-12-31 19:43:02 | Binance | 0.00100000 BTC |" + " 100.00000000 BNB | 0.10000000 BTC", + "message_with_keys": "2022-12-31 19:43:02 | Binance | PRICE: 0.00100000 BTC |" + " QTY: 100.00000000 BNB | TOTAL: 0.10000000 BTC", "exchange": "Binance", - "message": "2022-12-31 19:43:02 | Binance | 0.00100000 BTC | 100.00000000 BNB |" - " 0.10000000 BTC", - "message_with_keys": "2022-12-31 19:43:02 | Binance | PRICE: 0.00100000 BTC | " - " QTY: 100.00000000 BNB | TOTAL: 0.10000000 BTC", } @@ -66,12 +67,45 @@ def test_schemas_kucoin(): "quantity": Decimal("3.7335"), "trade_time": datetime.datetime(2023, 5, 3, 10, 10, 39), "total": Decimal("0.0000131344530"), - "is_seller": True, "currency": "BTC", "trade_symbol": "LTO", + "message": "2023-05-03 10:10:39 | Kucoin | 0.00000352 BTC |" + " 3.73350000 LTO | 0.00001313 BTC", + "message_with_keys": "2023-05-03 10:10:39 | Kucoin | PRICE: 0.00000352 BTC |" + " QTY: 3.73350000 LTO | TOTAL: 0.00001313 BTC", + "is_seller": True, "exchange": "Kucoin", - "message": "2023-05-03 10:10:39 | Kucoin | 0.00000352 BTC | 3.73350000 LTO |" - " 0.00001313 BTC", - "message_with_keys": "2023-05-03 10:10:39 | Kucoin | PRICE: 0.00000352 BTC | " - " QTY: 3.73350000 LTO | TOTAL: 0.00001313 BTC", + } + + +def test_schemas_coinbase(): + msg = { + "type": "last_match", + "trade_id": 461948687, + "maker_order_id": "335d96b3-e77a-411b-924c-a6acbbe8affb", + "taker_order_id": "ebe2f71e-513a-4084-97d1-4d5809be161b", + "side": "sell", + "size": "0.00251665", + "price": "1933.93", + "product_id": "ETH-USD", + "sequence": 47388972940, + "time": "2023-07-16T12:19:57.255067Z", + } + + payload = CoinbaseTradeSchema(**msg) + + assert payload.model_dump() == { + "symbol": "ETHUSD", + "price": Decimal("1933.93"), + "quantity": Decimal("0.00251665"), + "trade_time": datetime.datetime(2023, 7, 16, 12, 19, 57), + "total": Decimal("4.8670249345"), + "currency": "USD", + "trade_symbol": "ETH", + "message": "2023-07-16 12:19:57 | Coinbase | 1933.93000000 USD |" + " 0.00251665 ETH | 4.86702493 USD", + "message_with_keys": "2023-07-16 12:19:57 | Coinbase | PRICE: 1933.93000000 USD |" + " QTY: 0.00251665 ETH | TOTAL: 4.86702493 USD", + "is_seller": True, + "exchange": "Coinbase", } diff --git a/exchange_radar/web/src/settings/base.py b/exchange_radar/web/src/settings/base.py index bad7af2..0df3a01 100644 --- a/exchange_radar/web/src/settings/base.py +++ b/exchange_radar/web/src/settings/base.py @@ -5,6 +5,7 @@ DEBUG = env.bool("DEBUG") DB_PATH = env.str("DB_PATH") +DB_TABLE_MAX_ROWS = env.int("DB_TABLE_MAX_ROWS") TRADES_SOCKET_URL = env.str("TRADES_SOCKET_URL") TRADES_WHALES_SOCKET_URL = env.str("TRADES_WHALES_SOCKET_URL") @@ -16,4 +17,6 @@ TRADES_DOLPHINS_HOST_URL = env.str("TRADES_DOLPHINS_HOST_URL") TRADES_OCTOPUSES_HOST_URL = env.str("TRADES_OCTOPUSES_HOST_URL") -DB_TABLE_MAX_ROWS = env.int("DB_TABLE_MAX_ROWS") +BINANCE = env.list("BINANCE") +KUCOIN = env.list("KUCOIN") +COINBASE = env.list("COINBASE") diff --git a/exchange_radar/web/src/urls.py b/exchange_radar/web/src/urls.py index 30d2a1a..5424d0e 100644 --- a/exchange_radar/web/src/urls.py +++ b/exchange_radar/web/src/urls.py @@ -19,6 +19,7 @@ ) routes = [ + # Preferably should be served by a load-balancer and not this web-app Mount( "/static", StaticFiles(directory="./exchange_radar/web/static"), diff --git a/exchange_radar/web/src/utils.py b/exchange_radar/web/src/utils.py new file mode 100644 index 0000000..ea4fce8 --- /dev/null +++ b/exchange_radar/web/src/utils.py @@ -0,0 +1,14 @@ +from exchange_radar.web.src.settings import base as settings + + +def get_exchanges(coin: str) -> str: + txt = [] + + if coin in settings.BINANCE: + txt.append("Binance") + if coin in settings.KUCOIN: + txt.append("Kucoin") + if coin in settings.COINBASE: + txt.append("Coinbase") + + return ", ".join(txt) diff --git a/exchange_radar/web/src/views/http.py b/exchange_radar/web/src/views/http.py index 6cebd3b..b6c05a0 100644 --- a/exchange_radar/web/src/views/http.py +++ b/exchange_radar/web/src/views/http.py @@ -10,6 +10,7 @@ ) from exchange_radar.web.src.models import Db from exchange_radar.web.src.settings import base as settings +from exchange_radar.web.src.utils import get_exchanges templates = Jinja2Templates(directory="/app/exchange_radar/web/templates") @@ -32,6 +33,7 @@ def get(self, request): # noqa "coin": coin, "websocket_url": self.websocket_url.format(coin=coin), "http_url": self.http_url.format(coin=coin), + "exchanges": get_exchanges(coin=coin), "max_rows": settings.DB_TABLE_MAX_ROWS, } return templates.TemplateResponse(self.template_name, context=context) diff --git a/exchange_radar/web/static/css/custom.css b/exchange_radar/web/static/css/custom.css index dc1cbcd..0487127 100644 --- a/exchange_radar/web/static/css/custom.css +++ b/exchange_radar/web/static/css/custom.css @@ -1,5 +1,15 @@ body { - background: #e0e0e0; + background: var(--background-color); + color: var(--text-color); +} + +a { + color: var(--link-color); +} + +a:hover { + text-decoration: underline; + filter: brightness(80%); } #title { @@ -9,20 +19,15 @@ body { } .order_sell { - background-color: red; - color: white; + background-color: var(--order-sell-background-color); + color: var(--order-sell-text-color); } .order_buy { - background-color: green; - color: white; + background-color: var(--order-buy-background-color); + color: var(--order-buy-text-color); } .right { width: 42rem; } - -xpre { - overflow: auto; - white-space: pre-wrap; -} diff --git a/exchange_radar/web/static/css/exchanges.css b/exchange_radar/web/static/css/exchanges.css new file mode 100644 index 0000000..686d62b --- /dev/null +++ b/exchange_radar/web/static/css/exchanges.css @@ -0,0 +1,14 @@ +.binance { + background: var(--binance-background-color); + color: var(--binance-text-color); +} + +.kucoin { + background: var(--kucoin-background-color); + color: var(--kucoin-text-color); +} + +.coinbase { + background: var(--coinbase-background-color); + color: var(--coinbase-text-color); +} diff --git a/exchange_radar/web/static/css/style.css b/exchange_radar/web/static/css/style.css index 19d13c3..9b4e145 100644 --- a/exchange_radar/web/static/css/style.css +++ b/exchange_radar/web/static/css/style.css @@ -8,7 +8,6 @@ /* boxes */ .padded-boxes > * { width:100%; - xbackground:#eee; border-radius:.4rem; } diff --git a/exchange_radar/web/static/css/theme.css b/exchange_radar/web/static/css/theme.css new file mode 100644 index 0000000..c62394d --- /dev/null +++ b/exchange_radar/web/static/css/theme.css @@ -0,0 +1,91 @@ +:root { + --background-color: #e0e0e0; + --text-color: #121416d8; + --link-color: #543fd7; + --order-buy-text-color: #fff; + --order-buy-background-color: green; + --order-sell-text-color: #fff; + --order-sell-background-color: red; + --binance-background-color: #facf3a; + --binance-text-color: black; + --kucoin-background-color: #62dba4; + --kucoin-text-color: black; + --coinbase-background-color: #1449fc; + --coinbase-text-color: white; +} + +[data-theme="dark"] { + --background-color: #212a2e; + --text-color: #b8b8b8; + --link-color: #828fff; + --order-buy-text-color: #b8b8b8; + --order-buy-background-color: #3a5742; + --order-sell-text-color: #b8b8b8; + --order-sell-background-color: #573a3a; + --binance-background-color: #806a1c; + --binance-text-color: black; + --kucoin-background-color: #326b52; + --kucoin-text-color: #b8b8b8; + --coinbase-background-color: #132254; + --coinbase-text-color: #b8b8b8; +} + +/* toggle switch */ + +.theme-switch-wrapper { + display: flex; + align-items: center; +} + +em { + margin-left: 10px; +} + +.theme-switch { + display: inline-block; + height: 34px; + position: relative; + width: 60px; +} + +.theme-switch input { + display:none; +} + +.slider { + background-color: #ccc; + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: .4s; +} + +.slider:before { + background-color: #fff; + bottom: 4px; + content: ""; + height: 26px; + left: 4px; + position: absolute; + transition: .4s; + width: 26px; +} + +input:checked + .slider { + background-color: #66bb6a; +} + +input:checked + .slider:before { + transform: translateX(26px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} diff --git a/exchange_radar/web/templates/base.html b/exchange_radar/web/templates/base.html index 545d711..5260ef7 100644 --- a/exchange_radar/web/templates/base.html +++ b/exchange_radar/web/templates/base.html @@ -6,14 +6,19 @@ + + + + + + @@ -94,7 +125,7 @@
-
                           {% block heading %}{% endblock %}
+
 ------------------------------------------------------------------------------------------------------------
        DATETIME      | EXCHANGE |        PRICE        |          QUANTITY          |          TOTAL
 ------------------------------------------------------------------------------------------------------------
@@ -106,6 +137,10 @@
                     
 
 
+Exchanges:
+
+{{ exchanges }}
+
 
 Bitcoin:
 
@@ -151,10 +186,19 @@
 
 
 
+*octopus -> More than 1,000 USD/USDT or 0.04 BTC or 0.6 ETH in a single transaction
+*dolphin -> More than 10,000 USD/USDT or 0.4 BTC or 6 ETH in a single transaction
+*whale -> More than 100,000 USD/USDT or 4 BTC or 60 ETH in a single transaction
+
+
+
+ + Dark Mode +
-*octopus -> More than 1,000 USDT or 0.04 BTC or 0.6 ETH in a single transaction -*dolphin -> More than 10,000 USDT or 0.4 BTC or 6 ETH in a single transaction -*whale -> More than 100,000 USDT or 4 BTC or 60 ETH in a single transaction
diff --git a/exchange_radar/web/templates/index-dolphins.html b/exchange_radar/web/templates/index-dolphins.html index 5e31cae..9432c39 100644 --- a/exchange_radar/web/templates/index-dolphins.html +++ b/exchange_radar/web/templates/index-dolphins.html @@ -2,9 +2,8 @@ {% block title %}{{ coin }} dolphin{% endblock %} {% block javascript %} {% endblock %} -{% block heading %}{{ coin }} dolphin trades in Binance and Kucoin{% endblock %} diff --git a/exchange_radar/web/templates/index-octopuses.html b/exchange_radar/web/templates/index-octopuses.html index b09a666..9c802fa 100644 --- a/exchange_radar/web/templates/index-octopuses.html +++ b/exchange_radar/web/templates/index-octopuses.html @@ -2,9 +2,8 @@ {% block title %}{{ coin }} octopus{% endblock %} {% block javascript %} {% endblock %} -{% block heading %}{{ coin }} octopus trades in Binance and Kucoin{% endblock %} diff --git a/exchange_radar/web/templates/index-whales.html b/exchange_radar/web/templates/index-whales.html index de27e64..2c7d3b4 100644 --- a/exchange_radar/web/templates/index-whales.html +++ b/exchange_radar/web/templates/index-whales.html @@ -2,9 +2,8 @@ {% block title %}{{ coin }} whale{% endblock %} {% block javascript %} {% endblock %} -{% block heading %}{{ coin }} whale trades in Binance and Kucoin{% endblock %} diff --git a/exchange_radar/web/templates/index.html b/exchange_radar/web/templates/index.html index f655b13..cb40be5 100644 --- a/exchange_radar/web/templates/index.html +++ b/exchange_radar/web/templates/index.html @@ -2,9 +2,8 @@ {% block title %}{{ coin }}{% endblock %} {% block javascript %} {% endblock %} -{% block heading %}{{ coin }} all trades in Binance and Kucoin{% endblock %} diff --git a/local.yml b/local.yml index 9a5d76e..498eedf 100644 --- a/local.yml +++ b/local.yml @@ -57,6 +57,21 @@ services: - ./exchange_radar/producer:/app/exchange_radar/producer command: /start + producer-coinbase: + build: + context: . + dockerfile: compose/producer/coinbase/Dockerfile + image: exchange_radar_producer_coinbase + container_name: exchange-radar-producer-coinbase + env_file: + - ./.envs/.local/.rabbitmq + - ./.envs/.local/.producer + depends_on: + - rabbitmq + volumes: + - ./exchange_radar/producer:/app/exchange_radar/producer + command: /start + consumer: build: context: . diff --git a/poetry.lock b/poetry.lock index c75114b..3cbcda6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -192,6 +192,17 @@ files = [ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] +[[package]] +name = "asynctest" +version = "0.13.0" +description = "Enhance the standard unittest package with features for testing asyncio libraries" +optional = false +python-versions = ">=3.5" +files = [ + {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, + {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, +] + [[package]] name = "attrs" version = "23.1.0" @@ -210,6 +221,34 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "autobahn" +version = "23.6.2" +description = "WebSocket client & server library, WAMP real-time framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "autobahn-23.6.2.tar.gz", hash = "sha256:ec9421c52a2103364d1ef0468036e6019ee84f71721e86b36fe19ad6966c1181"}, +] + +[package.dependencies] +cryptography = ">=3.4.6" +hyperlink = ">=21.0.0" +setuptools = "*" +txaio = ">=21.2.1" + +[package.extras] +all = ["PyGObject (>=3.40.0)", "argon2_cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2)", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service_identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "ujson (>=4.0.2)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] +compress = ["python-snappy (>=0.6.0)"] +dev = ["backports.tempfile (>=1.0)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610)", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx_rtd_theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=4.2.8)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=22.10.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] +encryption = ["pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "service_identity (>=18.1.0)"] +nvx = ["cffi (>=1.14.5)"] +scram = ["argon2_cffi (>=20.1.0)", "cffi (>=1.14.5)", "passlib (>=1.7.4)"] +serialization = ["cbor2 (>=5.2.0)", "flatbuffers (>=22.12.6)", "msgpack (>=1.0.2)", "py-ubjson (>=0.16.1)", "ujson (>=4.0.2)"] +twisted = ["attrs (>=20.3.0)", "twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] +ui = ["PyGObject (>=3.40.0)"] +xbr = ["base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "rlp (>=2.0.1)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)"] + [[package]] name = "backcall" version = "0.2.0" @@ -232,6 +271,82 @@ files = [ {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, ] +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.3.1" @@ -329,13 +444,13 @@ files = [ [[package]] name = "click" -version = "8.1.5" +version = "8.1.6" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.5-py3-none-any.whl", hash = "sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"}, - {file = "click-8.1.5.tar.gz", hash = "sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367"}, + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] [package.dependencies] @@ -352,6 +467,24 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "copra" +version = "1.2.9" +description = "Asyncronous Python REST and WebSocket Clients for the Coinbase Pro virtual currency trading platform." +optional = false +python-versions = "*" +files = [ + {file = "copra-1.2.9-py3-none-any.whl", hash = "sha256:e13dbd7273c92fe25ae580556a66a4a536e00eb03204a3a7f0f7b234720cd60b"}, + {file = "copra-1.2.9.tar.gz", hash = "sha256:2ee52f5915bf7eeb0ad4a4789d4f365a821e65a2d62954107d3a43ff9dc1d035"}, +] + +[package.dependencies] +aiohttp = ">=3.4.4" +asynctest = "*" +autobahn = ">=18.8.1" +python-dateutil = "*" +python-dotenv = "*" + [[package]] name = "coverage" version = "7.2.7" @@ -424,6 +557,51 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "41.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, + {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, + {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, + {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "dateparser" version = "1.1.8" @@ -459,13 +637,13 @@ files = [ [[package]] name = "distlib" -version = "0.3.6" +version = "0.3.7" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] [[package]] @@ -646,6 +824,20 @@ files = [ [package.extras] test = ["Cython (>=0.29.24,<0.30.0)"] +[[package]] +name = "hyperlink" +version = "21.0.0" +description = "A featureful, immutable, and correct URL for Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, + {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, +] + +[package.dependencies] +idna = ">=2.5" + [[package]] name = "identify" version = "2.5.24" @@ -1100,6 +1292,17 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pycryptodome" version = "3.18.0" @@ -1360,51 +1563,51 @@ files = [ [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] @@ -1625,6 +1828,22 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] +[[package]] +name = "txaio" +version = "23.1.1" +description = "Compatibility API between asyncio/Twisted/Trollius" +optional = false +python-versions = ">=3.7" +files = [ + {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"}, + {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"}, +] + +[package.extras] +all = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] +dev = ["pep8 (>=1.6.2)", "pyenchant (>=1.6.6)", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "sphinx (>=1.2.3)", "sphinx-rtd-theme (>=0.1.9)", "sphinxcontrib-spelling (>=2.1.2)", "tox (>=2.1.1)", "tox-gh-actions (>=2.2.0)", "twine (>=1.6.5)", "wheel"] +twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] + [[package]] name = "typing-extensions" version = "4.7.1" @@ -2053,4 +2272,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "95a3f111d15fcda819ec1534a8bb00521d4768ef8dd0bf77eea393a52b39af46" +content-hash = "7f4b8158ad548d14cea205702138b4a4ba5b81bba59c79dab0e852f17804acbb" diff --git a/production.yml b/production.yml index 5da9836..7eaed6f 100644 --- a/production.yml +++ b/production.yml @@ -53,6 +53,19 @@ services: - rabbitmq command: /start + producer-coinbase: + build: + context: . + dockerfile: compose/producer/coinbase/Dockerfile + image: exchange_radar_producer_coinbase + container_name: exchange-radar-producer-coinbase + env_file: + - ./.envs/.production/.rabbitmq + - ./.envs/.production/.producer + depends_on: + - rabbitmq + command: /start + consumer: build: context: . diff --git a/pyproject.toml b/pyproject.toml index 2da9902..832f9af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,12 +44,11 @@ pika = "^1.3.2" requests = "^2.31.0" urllib3 = "^1.26.16" coverage = "^7.2.7" - +copra = "1.2.9" [tool.poetry.group.tests.dependencies] pytest = "^7.4.0" - [tool.poetry.group.dev.dependencies] pre-commit = "^3.3.3" ipython = "^8.14.0" @@ -58,11 +57,9 @@ ipython = "^8.14.0" requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" - [tool.pytest.ini_options] addopts = "-vv" - [tool.poetry.scripts] producer = "exchange_radar.producer.main:main" consumer = "exchange_radar.consumer.main:main" From aa2573bcdae13eef4e9d0ed39e6526d80ff01eb0 Mon Sep 17 00:00:00 2001 From: Paulo Antunes Date: Sun, 23 Jul 2023 16:12:42 +0200 Subject: [PATCH 2/3] Add Kraken & Code Refactor --- .envs/.local/.producer | 8 + .envs/.local/.web | 3 +- README.md | 5 +- compose/producer/kraken/Dockerfile | 47 ++++ compose/producer/kraken/start | 7 + exchange_radar/producer/publisher.py | 16 +- exchange_radar/producer/schemas/kraken.py | 35 +++ exchange_radar/producer/settings/exchanges.py | 2 + exchange_radar/producer/tasks/binance.py | 4 +- exchange_radar/producer/tasks/coinbase.py | 16 +- exchange_radar/producer/tasks/kraken.py | 58 ++++ exchange_radar/producer/tasks/kucoin.py | 8 +- .../producer/tasks/libs/__init__.py | 0 exchange_radar/producer/tasks/libs/kraken.py | 256 ++++++++++++++++++ exchange_radar/tests/producer/test_schemas.py | 39 +++ exchange_radar/web/src/settings/base.py | 3 +- exchange_radar/web/src/utils.py | 6 +- exchange_radar/web/static/css/custom.css | 2 + exchange_radar/web/static/css/exchanges.css | 5 + exchange_radar/web/static/css/theme.css | 4 + exchange_radar/web/templates/base.html | 4 +- local.yml | 15 + production.yml | 13 + pyproject.toml | 4 + 24 files changed, 534 insertions(+), 26 deletions(-) create mode 100644 compose/producer/kraken/Dockerfile create mode 100644 compose/producer/kraken/start create mode 100644 exchange_radar/producer/schemas/kraken.py create mode 100644 exchange_radar/producer/tasks/kraken.py create mode 100644 exchange_radar/producer/tasks/libs/__init__.py create mode 100644 exchange_radar/producer/tasks/libs/kraken.py diff --git a/.envs/.local/.producer b/.envs/.local/.producer index 430192c..3b71244 100644 --- a/.envs/.local/.producer +++ b/.envs/.local/.producer @@ -23,3 +23,11 @@ BTC-USD ETH-USD LINK-USD " + +KRAKEN_COINS=" +BTC/USD +ETH/USD ETH/BTC +LINK/USD +FTM/USD +PEPE/USD +" diff --git a/.envs/.local/.web b/.envs/.local/.web index 7f48d47..ef2da1a 100644 --- a/.envs/.local/.web +++ b/.envs/.local/.web @@ -14,5 +14,6 @@ TRADES_DOLPHINS_HOST_URL=http://127.0.0.1:9000/feed/{coin}/dolphins TRADES_OCTOPUSES_HOST_URL=http://127.0.0.1:9000/feed/{coin}/octopuses BINANCE=LTO,ETH,BTC,LINK,FTM,PEPE -KUCOIN=LTO,ETH,BTC,LINK,FTM,PEPE COINBASE=ETH,BTC,LINK +KRAKEN=ETH,BTC,LINK,FTM,PEPE +KUCOIN=LTO,ETH,BTC,LINK,FTM,PEPE diff --git a/README.md b/README.md index 2b6416d..a6db02b 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,11 @@ Get real-time trades from various crypto exchanges. So far the following crypto exchanges are supported: - Binance -- Kucoin - Coinbase +- Kraken +- Kucoin + +Currently, these are the top 4 exchanges by highest volume. ### Build & Run As simple as: diff --git a/compose/producer/kraken/Dockerfile b/compose/producer/kraken/Dockerfile new file mode 100644 index 0000000..2e06414 --- /dev/null +++ b/compose/producer/kraken/Dockerfile @@ -0,0 +1,47 @@ +ARG PYTHON_VERSION=3.11.3-slim-bullseye + +FROM python:${PYTHON_VERSION} as python + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + POETRY_HOME="/opt/poetry" \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 \ + PYSETUP_PATH="/app" \ + VENV_PATH="/app/.venv" + +ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" + + +FROM python as python-build-stage + +RUN apt-get update && apt-get install --no-install-recommends -y \ + curl \ + build-essential + +ENV POETRY_VERSION=1.5.1 +RUN curl -sSL https://install.python-poetry.org | python + +WORKDIR $PYSETUP_PATH + +COPY ./poetry.lock ./pyproject.toml ./ +RUN poetry install --only main --no-root + + +FROM python as python-run-stage + +COPY --from=python-build-stage $POETRY_HOME $POETRY_HOME +COPY --from=python-build-stage $PYSETUP_PATH $PYSETUP_PATH + +COPY ./compose/producer/kraken/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +WORKDIR $PYSETUP_PATH + +COPY . . + +RUN poetry install diff --git a/compose/producer/kraken/start b/compose/producer/kraken/start new file mode 100644 index 0000000..5ad5af4 --- /dev/null +++ b/compose/producer/kraken/start @@ -0,0 +1,7 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +exec poetry run producer ${KRAKEN_COINS} --exchange Kraken diff --git a/exchange_radar/producer/publisher.py b/exchange_radar/producer/publisher.py index 1db497c..46ec4a1 100644 --- a/exchange_radar/producer/publisher.py +++ b/exchange_radar/producer/publisher.py @@ -10,9 +10,7 @@ ) from exchange_radar.producer.enums import Ranking -from exchange_radar.producer.schemas.binance import BinanceTradeSchema -from exchange_radar.producer.schemas.coinbase import CoinbaseTradeSchema -from exchange_radar.producer.schemas.kucoin import KucoinTradeSchema +from exchange_radar.producer.schemas.base import CustomBaseModel from exchange_radar.producer.settings import base as settings from exchange_radar.producer.utils import get_ranking @@ -74,8 +72,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): producer_connection = ProducerConnection() -def publish(data: BinanceTradeSchema | KucoinTradeSchema | CoinbaseTradeSchema) -> None: - logger.info(f"PUB: {data.trade_time} {data.symbol}") +def publish(data: type[CustomBaseModel]) -> None: + logger.info(f"PUB: {data.trade_time} {data.symbol}") # noqa try: with ProducerChannel(queue_name=settings.RABBITMQ_TRADES_QUEUE_NAME) as channel: @@ -89,11 +87,11 @@ def publish(data: BinanceTradeSchema | KucoinTradeSchema | CoinbaseTradeSchema) ) queue_name = None - if get_ranking(data) == Ranking.WHALE: + if get_ranking(data) == Ranking.WHALE: # noqa queue_name = settings.RABBITMQ_TRADES_WHALES_QUEUE_NAME - elif get_ranking(data) == Ranking.DOLPHIN: + elif get_ranking(data) == Ranking.DOLPHIN: # noqa queue_name = settings.RABBITMQ_TRADES_DOLPHIN_QUEUE_NAME - elif get_ranking(data) == Ranking.OCTOPUS: + elif get_ranking(data) == Ranking.OCTOPUS: # noqa queue_name = settings.RABBITMQ_TRADES_OCTOPUS_QUEUE_NAME if queue_name is not None: @@ -117,4 +115,4 @@ def publish(data: BinanceTradeSchema | KucoinTradeSchema | CoinbaseTradeSchema) except Exception as error: logger.error(f"ERROR: {error}") else: - logger.info(f"PUB OK: {data.trade_time} {data.symbol}") + logger.info(f"PUB OK: {data.trade_time} {data.symbol}") # noqa diff --git a/exchange_radar/producer/schemas/kraken.py b/exchange_radar/producer/schemas/kraken.py new file mode 100644 index 0000000..d2686db --- /dev/null +++ b/exchange_radar/producer/schemas/kraken.py @@ -0,0 +1,35 @@ +from datetime import datetime +from functools import cached_property + +from pydantic import Field, computed_field, condecimal, field_validator + +from exchange_radar.producer.schemas.base import CustomBaseModel + + +class KrakenTradeSchema(CustomBaseModel): + symbol: str + price: condecimal(ge=0, decimal_places=12) + quantity: condecimal(ge=0, decimal_places=9) + trade_time: datetime + side: str = Field(exclude=True) + + @field_validator("symbol") + def symbol_normalization(cls, v) -> str: + if v[:4] == "XBT/": + v = v.replace("XBT/", "BTC/") + elif v[-4:] == "/XBT": + v = v.replace("/XBT", "/BTC") + return v.replace("/", "") + + @field_validator("trade_time", mode="before") + def trade_time_before(cls, v) -> int: + return int(v[:10]) + + @computed_field + @cached_property + def is_seller(self) -> bool: + return True if self.side == "s" else False + + @computed_field + def exchange(self) -> str: + return "Kraken" diff --git a/exchange_radar/producer/settings/exchanges.py b/exchange_radar/producer/settings/exchanges.py index bccdaae..510f8a0 100644 --- a/exchange_radar/producer/settings/exchanges.py +++ b/exchange_radar/producer/settings/exchanges.py @@ -1,9 +1,11 @@ from exchange_radar.producer.tasks.binance import BinanceTradesTask from exchange_radar.producer.tasks.coinbase import CoinbaseTradesTask +from exchange_radar.producer.tasks.kraken import KrakenTradesTask from exchange_radar.producer.tasks.kucoin import KucoinTradesTask EXCHANGES = { "Binance": BinanceTradesTask, "Kucoin": KucoinTradesTask, "Coinbase": CoinbaseTradesTask, + "Kraken": KrakenTradesTask, } diff --git a/exchange_radar/producer/tasks/binance.py b/exchange_radar/producer/tasks/binance.py index 747136e..b6074ee 100644 --- a/exchange_radar/producer/tasks/binance.py +++ b/exchange_radar/producer/tasks/binance.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -ITER_SLEEP = 10 * 60.0 +ITER_SLEEP = 10.0 class BinanceTradesTask(Task): @@ -31,6 +31,6 @@ async def process(self, symbol_or_symbols: str | tuple): res = await ts.recv() try: data = BinanceTradeSchema(**res) - publish(data) + publish(data) # noqa except Exception as error: logger.error(f"ERROR: {error}") diff --git a/exchange_radar/producer/tasks/coinbase.py b/exchange_radar/producer/tasks/coinbase.py index e04de3f..d3f368e 100644 --- a/exchange_radar/producer/tasks/coinbase.py +++ b/exchange_radar/producer/tasks/coinbase.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -ITER_SLEEP = 10 * 60.0 +ITER_SLEEP = 10.0 class CoinbaseTradesTask(Task): @@ -19,11 +19,15 @@ class CoinbaseTradesTask(Task): def process(self, symbol_or_symbols: str | tuple): class CustomClient(Client): def on_message(self, message): - try: - data = CoinbaseTradeSchema(**message) - publish(data) - except Exception as _error: - logger.error(f"ERROR: {_error}") + match message: + case {"type": "subscriptions"}: + pass + case _: + try: + data = CoinbaseTradeSchema(**message) + publish(data) # noqa + except Exception as _error: + logger.error(f"ERROR: {_error}") while True: try: diff --git a/exchange_radar/producer/tasks/kraken.py b/exchange_radar/producer/tasks/kraken.py new file mode 100644 index 0000000..d1c6879 --- /dev/null +++ b/exchange_radar/producer/tasks/kraken.py @@ -0,0 +1,58 @@ +import asyncio +import logging + +from exchange_radar.producer.publisher import publish +from exchange_radar.producer.schemas.kraken import KrakenTradeSchema +from exchange_radar.producer.task import Task +from exchange_radar.producer.tasks.libs.kraken import WSKrakenClient, WSKrakenInMsg + +logger = logging.getLogger(__name__) + + +ITER_SLEEP = 10.0 + + +class KrakenTradesTask(Task): + async def task(self, symbols: tuple[str]): + await asyncio.gather(self.process(symbols)) + + async def process(self, symbol_or_symbols: str | tuple): + async def recv_msgs(websocket): + async for msg in websocket: + await handle_msg(WSKrakenInMsg(msg)) + + async def handle_msg(msg: WSKrakenInMsg): + match msg.payload: + case {"event": "heartbeat"}: + pass + case {"event": "systemStatus"}: + pass + case {"event": "subscriptionStatus"}: + pass + case _: + try: + _, trades, _, symbol = msg.payload + for trade in trades: + price, volume, time, side, order_type, misc = trade + data = KrakenTradeSchema( + symbol=symbol, + price=price, + quantity=volume, + trade_time=time, + side=side, + ) + publish(data) # noqa + except Exception as error1: + logger.error(f"ERROR(1): {error1}") + + while True: + try: + async with WSKrakenClient(open_auth_socket=False) as client: + recv_task = asyncio.create_task(recv_msgs(client.websocket)) + await client.subscribe_trade(pair=list(symbol_or_symbols)) + await recv_task + except Exception as error2: + logger.error(f"ERROR(2): {error2}") + finally: + logger.error(f"Trying again in {ITER_SLEEP} seconds...") + await asyncio.sleep(ITER_SLEEP) diff --git a/exchange_radar/producer/tasks/kucoin.py b/exchange_radar/producer/tasks/kucoin.py index 36d8fb8..34578fc 100644 --- a/exchange_radar/producer/tasks/kucoin.py +++ b/exchange_radar/producer/tasks/kucoin.py @@ -12,6 +12,9 @@ logger = logging.getLogger(__name__) +ITER_SLEEP = 10.0 + + class KucoinTradesTask(Task): def __init__(self): super().__init__() @@ -37,7 +40,7 @@ async def callback(res): try: data = KucoinTradeSchema(**res["data"]) - publish(data) + publish(data) # noqa except Exception as error1: logger.error(f"ERROR(1): {error1}") @@ -47,7 +50,8 @@ async def callback(res): self.num_events += 1 if self.num_events <= 2: - await asyncio.sleep(20) + logger.error(f"Trying again in {ITER_SLEEP} seconds...") + await asyncio.sleep(ITER_SLEEP) continue try: diff --git a/exchange_radar/producer/tasks/libs/__init__.py b/exchange_radar/producer/tasks/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/exchange_radar/producer/tasks/libs/kraken.py b/exchange_radar/producer/tasks/libs/kraken.py new file mode 100644 index 0000000..06b062e --- /dev/null +++ b/exchange_radar/producer/tasks/libs/kraken.py @@ -0,0 +1,256 @@ +from __future__ import annotations + +import json + +import websockets + + +class WSKrakenOutMsg: + def __init__(self, payload: str | dict) -> None: + self.payload = payload if type(payload) is str else json.dumps(payload) + + +class WSKrakenInMsg: + def __init__(self, payload: str) -> None: + self.payload = json.loads(payload) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(payload={self.payload})" + + +class WSKrakenClient: + def __init__( + self, + uri: str = "wss://ws.kraken.com", + auth_uri: str = "wss://ws-auth.kraken.com", + open_auth_socket: bool = True, + ) -> None: + self.uri = uri + self.auth_uri = auth_uri + self.open_auth_socket = open_auth_socket + + async def send(self, msg: WSKrakenOutMsg) -> None: + await self._raw_send(msg.payload) + + async def _raw_send(self, *args, **kwargs) -> None: + await self.websocket.send(*args, **kwargs) + + async def auth_send(self, msg: WSKrakenOutMsg) -> None: + await self._raw_auth_send(msg.payload) + + async def _raw_auth_send(self, *args, **kwargs) -> None: + await self.auth_websocket.send(*args, **kwargs) + + async def recv(self) -> WSKrakenInMsg: + return WSKrakenInMsg(await self._raw_recv()) + + async def _raw_recv(self) -> str | bytes: + return await self.websocket.recv() + + async def auth_recv(self) -> WSKrakenInMsg: + return WSKrakenInMsg(await self._raw_auth_recv()) + + async def _raw_auth_recv(self) -> str | bytes: + return await self.auth_websocket.recv() + + async def __aenter__(self) -> WSKrakenClient: + self.websocket: websockets.WebSocketClientProtocol = await websockets.connect( + self.uri + ) + if self.open_auth_socket: + print( + "You are also opening an authenticated socket. At least one " + "private message should be subscribed to keep the authenticated " + "client connection open." + ) + self.auth_websocket: websockets.WebSocketClientProtocol = ( + await websockets.connect(self.auth_uri) + ) + return self + + async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: + await self.websocket.close() + + async def ping(self) -> None: + await self.send(WSKrakenOutMsg({"event": "ping"})) + + async def subscribe_custom(self, payload) -> None: + assert payload["event"] == "subscribe" + await self.send(WSKrakenOutMsg(payload)) + + async def unsubscribe_custom(self, payload) -> None: + assert payload["event"] == "unsubscribe" + await self.send(WSKrakenOutMsg(payload)) + + def _gen_raw_subscription_payload( + self, + event: str, + sub_name: str, + reqid: int = None, + pair: list[str] = None, + sub_depth: int = None, + sub_interval: int = None, + sub_ratecounter: int = None, + sub_snapshot: bool = None, + sub_token: str = None, + ) -> dict: + _locals = locals() + del _locals["self"] + payload = { + k: v + for (k, v) in _locals.items() + if not k.startswith("sub") and v is not None + } + payload["subscription"] = { + k[4:]: v + for (k, v) in _locals.items() + if k.startswith("sub") and v is not None + } + return payload + + def _gen_raw_subscribe_payload(self, **kwargs) -> dict: + return self._gen_raw_subscription_payload(event="subscribe", **kwargs) + + def _gen_raw_unsubscribe_payload(self, **kwargs) -> dict: + return self._gen_raw_subscription_payload(event="unsubscribe", **kwargs) + + async def subscribe_ticker(self, pair: list[str], **kwargs) -> None: + await self.send( + WSKrakenOutMsg( + self._gen_raw_subscribe_payload(sub_name="ticker", pair=pair, **kwargs) + ) + ) + + async def unsubscribe_ticker(self, pair: list[str], **kwargs) -> None: + await self.send( + WSKrakenOutMsg( + self._gen_raw_unsubscribe_payload( + sub_name="ticker", pair=pair, **kwargs + ) + ) + ) + + async def subscribe_trade(self, pair: list[str], **kwargs) -> None: + await self.send( + WSKrakenOutMsg( + self._gen_raw_subscribe_payload(sub_name="trade", pair=pair, **kwargs) + ) + ) + + async def unsubscribe_trade(self, pair: list[str], **kwargs) -> None: + await self.send( + WSKrakenOutMsg( + self._gen_raw_unsubscribe_payload(sub_name="trade", pair=pair, **kwargs) + ) + ) + + async def subscribe_ohlc(self, pair: list[str], interval: int) -> None: + await self.send( + WSKrakenOutMsg( + self._gen_raw_subscribe_payload( + sub_name="ohlc", pair=pair, sub_interval=interval + ) + ) + ) + + async def unsubscribe_ohlc(self, pair: list[str], interval: int) -> None: + await self.send( + WSKrakenOutMsg( + self._gen_raw_unsubscribe_payload( + sub_name="ohlc", pair=pair, sub_interval=interval + ) + ) + ) + + async def subscribe_own_trades( + self, token: str, snapshot: bool = True, **kwargs + ) -> None: + await self.auth_send( + WSKrakenOutMsg( + self._gen_raw_subscribe_payload( + sub_name="ownTrades", + sub_token=token, + sub_snapshot=snapshot, + **kwargs, + ) + ) + ) + + async def unsubscribe_own_trades(self, token: str, **kwargs) -> None: + await self.auth_send( + WSKrakenOutMsg( + self._gen_raw_unsubscribe_payload( + sub_name="ownTrades", sub_token=token, **kwargs + ) + ) + ) + + async def subscribe_open_orders( + self, token: str, ratecounter: bool = False, **kwargs + ) -> None: + await self.auth_send( + WSKrakenOutMsg( + self._gen_raw_subscribe_payload( + sub_name="openOrders", + sub_token=token, + sub_ratecounter=ratecounter, + **kwargs, + ) + ) + ) + + async def unsubscribe_open_orders( + self, token: str, ratecounter: bool = False, **kwargs + ) -> None: + await self.auth_send( + WSKrakenOutMsg( + self._gen_raw_unsubscribe_payload( + sub_name="openOrders", + sub_token=token, + sub_ratecounter=ratecounter, + **kwargs, + ) + ) + ) + + async def add_order( + self, + token: str, + ordertype: str, + type: str, + pair: str, + volume: str, + price: str = None, + price2: str = None, + leverage: int = None, + oflags: str = None, + starttm: str = None, + expiretm: str = None, + deadline: str = None, + userref: str = None, + validate: str = None, + close_ordertype: str = None, + close_price: str = None, + close_price2: str = None, + timeinforce: str = None, + reqid: int = None, + ) -> None: + _locals = locals() + del _locals["self"] + + def rename_key(k): + key_to_new = { + "close_ordertype": "close[ordertype]", + "close_price": "close[price]", + "close_price2": "close[price2]", + } + return key_to_new.get(k, k) + + payload = {rename_key(k): v for (k, v) in _locals.items() if v is not None} + payload["event"] = "addOrder" + await self.auth_send(WSKrakenOutMsg(payload)) + + async def cancel_order(self, token: str, txid: list[str]): + await self.auth_send( + WSKrakenOutMsg(dict(event="cancelOrder", token=token, txid=txid)) + ) diff --git a/exchange_radar/tests/producer/test_schemas.py b/exchange_radar/tests/producer/test_schemas.py index 61aeea5..239f723 100644 --- a/exchange_radar/tests/producer/test_schemas.py +++ b/exchange_radar/tests/producer/test_schemas.py @@ -3,6 +3,7 @@ from exchange_radar.producer.schemas.binance import BinanceTradeSchema from exchange_radar.producer.schemas.coinbase import CoinbaseTradeSchema +from exchange_radar.producer.schemas.kraken import KrakenTradeSchema from exchange_radar.producer.schemas.kucoin import KucoinTradeSchema @@ -109,3 +110,41 @@ def test_schemas_coinbase(): "is_seller": True, "exchange": "Coinbase", } + + +def test_schemas_kraken(): + msg = [ + 337, + [["29911.20000", "0.03409475", "1690115020.186705", "s", "l", ""]], + "trade", + "XBT/USD", + ] + + _, trades, _, symbol = msg + trade = trades[0] + + price, volume, time, side, order_type, misc = trade + + payload = KrakenTradeSchema( + symbol=symbol, + price=price, + quantity=volume, + trade_time=time, + side=side, + ) + + assert payload.model_dump() == { + "symbol": "BTCUSD", + "price": Decimal("29911.20000"), + "quantity": Decimal("0.03409475"), + "trade_time": datetime.datetime(2023, 7, 23, 12, 23, 40), + "total": Decimal("1019.8148862000"), + "currency": "USD", + "trade_symbol": "BTC", + "message": "2023-07-23 12:23:40 | Kraken | 29911.20000000 USD |" + " 0.03409475 BTC | 1019.81488620 USD", + "message_with_keys": "2023-07-23 12:23:40 | Kraken | PRICE: 29911.20000000 USD |" + " QTY: 0.03409475 BTC | TOTAL: 1019.81488620 USD", + "is_seller": True, + "exchange": "Kraken", + } diff --git a/exchange_radar/web/src/settings/base.py b/exchange_radar/web/src/settings/base.py index 0df3a01..3e09707 100644 --- a/exchange_radar/web/src/settings/base.py +++ b/exchange_radar/web/src/settings/base.py @@ -18,5 +18,6 @@ TRADES_OCTOPUSES_HOST_URL = env.str("TRADES_OCTOPUSES_HOST_URL") BINANCE = env.list("BINANCE") -KUCOIN = env.list("KUCOIN") COINBASE = env.list("COINBASE") +KRAKEN = env.list("KRAKEN") +KUCOIN = env.list("KUCOIN") diff --git a/exchange_radar/web/src/utils.py b/exchange_radar/web/src/utils.py index ea4fce8..9d8564b 100644 --- a/exchange_radar/web/src/utils.py +++ b/exchange_radar/web/src/utils.py @@ -6,9 +6,11 @@ def get_exchanges(coin: str) -> str: if coin in settings.BINANCE: txt.append("Binance") - if coin in settings.KUCOIN: - txt.append("Kucoin") if coin in settings.COINBASE: txt.append("Coinbase") + if coin in settings.KRAKEN: + txt.append("Kraken") + if coin in settings.KUCOIN: + txt.append("Kucoin") return ", ".join(txt) diff --git a/exchange_radar/web/static/css/custom.css b/exchange_radar/web/static/css/custom.css index 0487127..4c16054 100644 --- a/exchange_radar/web/static/css/custom.css +++ b/exchange_radar/web/static/css/custom.css @@ -1,5 +1,7 @@ body { background: var(--background-color); + font-family: monospace, courier, "courier new"; + font-size: 13px; color: var(--text-color); } diff --git a/exchange_radar/web/static/css/exchanges.css b/exchange_radar/web/static/css/exchanges.css index 686d62b..a4d7629 100644 --- a/exchange_radar/web/static/css/exchanges.css +++ b/exchange_radar/web/static/css/exchanges.css @@ -12,3 +12,8 @@ background: var(--coinbase-background-color); color: var(--coinbase-text-color); } + +.kraken { + background: var(--kraken-background-color); + color: var(--kraken-text-color); +} diff --git a/exchange_radar/web/static/css/theme.css b/exchange_radar/web/static/css/theme.css index c62394d..6b88123 100644 --- a/exchange_radar/web/static/css/theme.css +++ b/exchange_radar/web/static/css/theme.css @@ -12,6 +12,8 @@ --kucoin-text-color: black; --coinbase-background-color: #1449fc; --coinbase-text-color: white; + --kraken-background-color: #5442d0; + --kraken-text-color: white; } [data-theme="dark"] { @@ -28,6 +30,8 @@ --kucoin-text-color: #b8b8b8; --coinbase-background-color: #132254; --coinbase-text-color: #b8b8b8; + --kraken-background-color: #31267a; + --kraken-text-color: #b8b8b8; } /* toggle switch */ diff --git a/exchange_radar/web/templates/base.html b/exchange_radar/web/templates/base.html index 5260ef7..44e9229 100644 --- a/exchange_radar/web/templates/base.html +++ b/exchange_radar/web/templates/base.html @@ -12,7 +12,7 @@ - + @@ -96,7 +96,6 @@ if (currentTheme) { document.documentElement.setAttribute('data-theme', currentTheme); - console.log(document.documentElement.getAttribute('data-theme')); if (currentTheme === 'dark') { toggleSwitch.checked = true; @@ -200,6 +199,7 @@ + Contact diff --git a/local.yml b/local.yml index 498eedf..874784a 100644 --- a/local.yml +++ b/local.yml @@ -72,6 +72,21 @@ services: - ./exchange_radar/producer:/app/exchange_radar/producer command: /start + producer-kraken: + build: + context: . + dockerfile: compose/producer/kraken/Dockerfile + image: exchange_radar_producer_kraken + container_name: exchange-radar-producer-kraken + env_file: + - ./.envs/.local/.rabbitmq + - ./.envs/.local/.producer + depends_on: + - rabbitmq + volumes: + - ./exchange_radar/producer:/app/exchange_radar/producer + command: /start + consumer: build: context: . diff --git a/production.yml b/production.yml index 7eaed6f..1f24eaf 100644 --- a/production.yml +++ b/production.yml @@ -66,6 +66,19 @@ services: - rabbitmq command: /start + producer-kraken: + build: + context: . + dockerfile: compose/producer/kraken/Dockerfile + image: exchange_radar_producer_kraken + container_name: exchange-radar-producer-kraken + env_file: + - ./.envs/.production/.rabbitmq + - ./.envs/.production/.producer + depends_on: + - rabbitmq + command: /start + consumer: build: context: . diff --git a/pyproject.toml b/pyproject.toml index 832f9af..1b8581a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,10 @@ keywords=[ "crypto exchanges", "exchange", "crypto", + "binance", + "coinbase", + "kraken", + "kucoin", ] classifiers=[ "Development Status :: 5 - Production/Stable", From e4063369bc70e41a0905c61509450188bacda81e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 00:56:46 +0000 Subject: [PATCH 3/3] Bump urllib3 from 1.26.16 to 1.26.18 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.16 to 1.26.18. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.16...1.26.18) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 32 ++++++++++++++++++++++++++------ pyproject.toml | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3cbcda6..ef265cb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -975,6 +975,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1573,6 +1583,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1580,8 +1591,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1598,6 +1616,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1605,6 +1624,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1955,17 +1975,17 @@ files = [ [[package]] name = "urllib3" -version = "1.26.16" +version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, - {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] @@ -2272,4 +2292,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "7f4b8158ad548d14cea205702138b4a4ba5b81bba59c79dab0e852f17804acbb" +content-hash = "d4ba7ab597b00fa16fd45fc9f9bafad623ad0ff5ca636af0c47c566f6424fc29" diff --git a/pyproject.toml b/pyproject.toml index 1b8581a..b936d66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ pydantic = "2.0a3" click = "^8.1.5" pika = "^1.3.2" requests = "^2.31.0" -urllib3 = "^1.26.16" +urllib3 = "^1.26.18" coverage = "^7.2.7" copra = "1.2.9"