Skip to content

Commit

Permalink
Release v1.1.0 (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
urischwartz-cb authored Jan 31, 2024
1 parent 0b7522d commit 36f0f02
Show file tree
Hide file tree
Showing 26 changed files with 1,595 additions and 27 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [1.1.0] - 2024-FEB-1 # TODO: Update this date

### Added
- Initial release of WebSocket API client
- Verbose logging option for RESTClient

## [1.0.4] - 2024-JAN-29

### Fixed
Expand Down
140 changes: 137 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![Code Style](https://img.shields.io/badge/code_style-black-black)](https://black.readthedocs.io/en/stable/)

Welcome to the official Coinbase Advanced API Python SDK. This python project was created to allow coders to easily plug into the [Coinbase Advanced API](https://docs.cloud.coinbase.com/advanced-trade-api/docs/welcome).
This SDK also supports easy connection to the [Coinbase Advanced Trade WebSocket API](https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview).

## Installation

Expand Down Expand Up @@ -54,7 +55,7 @@ You can also set a timeout in seconds for your REST requests like so:
client = RESTClient(api_key=api_key, api_secret=api_secret, timeout=5)
```

### Using the Client
### Using the REST Client

You are able to use any of the API hooks to make calls to the Coinbase API. For example:
```python
Expand Down Expand Up @@ -96,8 +97,141 @@ market_trades = client.get_market_trades(product_id="BTC-USD", limit=5)
portfolio = client.create_portfolio(name="TestPortfolio")
```

## WebSocket API Client
We offer a WebSocket API client that allows you to connect to the [Coinbase Advanced Trade WebSocket API](https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview).
Refer to the [Advanced Trade WebSocket Channels](https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels) page for detailed information on each offered channel.

In your code, import the WSClient class and instantiate it. The WSClient requires an API key and secret to be passed in as arguments. You can also use a key file or environment variables as described in the RESTClient instructions above.

You must specify an `on_message` function that will be called when a message is received from the WebSocket API. This function must take in a single argument, which will be the raw message received from the WebSocket API. For example:
```python
from coinbase.websocket import WSClient

api_key = "organizations/{org_id}/apiKeys/{key_id}"
api_secret = "-----BEGIN EC PRIVATE KEY-----\nYOUR PRIVATE KEY\n-----END EC PRIVATE KEY-----\n"

def on_message(msg):
print(msg)

client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message)
```
In this example, the `on_message` function simply prints the message received from the WebSocket API.

You can also set a `timeout` in seconds for your WebSocket connection, as well as a `max_size` in bytes for the messages received from the WebSocket API.
```python
client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message, timeout=5, max_size=65536) # 64 KB max_size
```
Other configurable fields are the `on_open` and `on_close` functions. If provided, these are called when the WebSocket connection is opened or closed, respectively. For example:
```python
def on_open():
print("Connection opened!")

client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message, on_open=on_open)
```

### Using the WebSocket Client
Once you have instantiated the client, you can connect to the WebSocket API by calling the `open` method, and disconnect by calling the `close` method.
The `subscribe` method allows you to subscribe to specific channels, for specific products. Similarly, the `unsubscribe` method allows you to unsubscribe from specific channels, for specific products. For example:

```python
# open the connection and subscribe to the ticker and heartbeat channels for BTC-USD and ETH-USD
client.open()
client.subscribe(product_ids=["BTC-USD", "ETH-USD"], channels=["ticker", "heartbeats"])

# wait 10 seconds
time.sleep(10)

# unsubscribe from the ticker channel and heartbeat channels for BTC-USD and ETH-USD, and close the connection
client.unsubscribe(product_ids=["BTC-USD", "ETH-USD"], channels=["ticker", "heartbeats"])
client.close()
```

We also provide channel specific methods for subscribing and unsubscribing. For example, the below code is equivalent to the example from above:
```python
client.open()
client.ticker(product_ids=["BTC-USD", "ETH-USD"])
client.heartbeats(product_ids=["BTC-USD", "ETH-USD"])

# wait 10 seconds
time.sleep(10)

client.ticker_unsubscribe(product_ids=["BTC-USD", "ETH-USD"])
client.heartbeats_unsubscribe(product_ids=["BTC-USD", "ETH-USD"])
client.close()
```

### Automatic Reconnection to the WebSocket API
The WebSocket client will automatically attempt to reconnect the WebSocket API if the connection is lost, and will resubscribe to any channels that were previously subscribed to.

The client uses an exponential backoff algorithm to determine how long to wait before attempting to reconnect, with a maximum number of retries of 5.

If you do not want to automatically reconnect, you can set the `retry` argument to `False` when instantiating the client.
```python
client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message, retry=False)
```

### Catching WebSocket Exceptions
The WebSocket API client will raise exceptions if it encounters an error. On forced disconnects it will raise a `WSClientConnectionClosedException`, otherwise it will raise a `WSClientException`.

NOTE: Errors on forced disconnects, or within logic in the message handler, will not be automatically raised since this will be running on its own thread.

We provide the `sleep_with_exception_check` and `run_forever_with_exception_check` methods to allow you to catch these exceptions. `sleep_with_exception_check` will sleep for the specified number of seconds, and will check for any exception raised during that time. `run_forever_with_exception_check` will run forever, checking for exceptions every second. For example:

```python
from coinbase.websocket import (WSClient, WSClientConnectionClosedException,
WSClientException)

client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message)

try:
client.open()
client.subscribe(product_ids=["BTC-USD", "ETH-USD"], channels=["ticker", "heartbeats"])
client.run_forever_with_exception_check()
except WSClientConnectionClosedException as e:
print("Connection closed! Retry attempts exhausted.")
except WSClientException as e:
print("Error encountered!")
```

This code will open the connection, subscribe to the ticker and heartbeat channels for BTC-USD and ETH-USD, and will sleep forever, checking for exceptions every second. If an exception is raised, it will be caught and handled appropriately.

If you only want to run for 5 seconds, you can use `sleep_with_exception_check`:
```python
client.sleep_with_exception_check(sleep=5)
```

Note that if the automatic reconnection fails after the retry limit is reached, a `WSClientConnectionClosedException` will be raised.

If you wish to implement your own reconnection logic, you can catch the `WSClientConnectionClosedException` and handle it appropriately. For example:
```python
client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message, retry=False)

def connect_and_subscribe():
try:
client.open()
client.subscribe(product_ids=["BTC-USD", "ETH-USD"], channels=["ticker", "heartbeats"])
client.run_forever_with_exception_check()
except WSClientConnectionClosedException as e:
print("Connection closed! Sleeping for 20 seconds before reconnecting...")
time.sleep(20)
connect_and_subscribe()
```

### Async WebSocket Client
The functions described above handle the asynchronous nature of WebSocket connections for you. However, if you wish to handle this yourself, you can use the `async_open`, `async_subscribe`, `async_unsubscribe`, and `async_close` methods.

We similarly provide async channel specific methods for subscribing and unsubscribing such as `ticker_async`, `ticker_unsubscribe_async`, etc.

## Debugging the Clients
You can enable debug logging for the REST and WebSocket clients by setting the `verbose` variable to `True` when initializing the clients. This will log useful information throughout the lifecycle of the REST request or WebSocket connection, and is highly recommended for debugging purposes.
```python
rest_client = RESTClient(api_key=api_key, api_secret=api_secret, verbose=True)

ws_client = WSClient(api_key=api_key, api_secret=api_secret, on_message=on_message, verbose=True)
```

## Authentication
Authentication of Cloud API Keys is handled automatically by the SDK when making a REST request.
Authentication of Cloud API Keys is handled automatically by the SDK when making a REST request or sending a WebSocket message.

However, if you wish to handle this yourself, you must create a JWT token and attach it to your request as detailed in the Cloud API docs [here](https://docs.cloud.coinbase.com/advanced-trade-api/docs/rest-api-auth#making-requests). Use the built in `jwt_generator` to create your JWT token. For example:
```python
Expand Down Expand Up @@ -128,7 +262,7 @@ jwt = jwt_generator.build_ws_jwt(api_key, api_secret)
Use this JWT to connect to the Websocket API by setting it in the "jwt" field of your subscription requests. See the docs [here](https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#sending-messages-using-cloud-api-keys) for more details.

## Changelog
For a detailed list of changes, see the [Changelog](CHANGELOG.md).
For a detailed list of changes, see the [Changelog](https://github.com/coinbase/coinbase-advanced-py/blob/master/CHANGELOG.md).

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion coinbase/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.4"
__version__ = "1.1.0"
18 changes: 17 additions & 1 deletion coinbase/api_base.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import json
import logging
import os
from typing import IO, Optional, Union

from coinbase.constants import API_ENV_KEY, API_SECRET_ENV_KEY


def get_logger(name):
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)

handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)

return logger


class APIBase(object):
def __init__(
self,
api_key: Optional[str] = os.getenv(API_ENV_KEY),
api_secret: Optional[str] = os.getenv(API_SECRET_ENV_KEY),
key_file: Optional[Union[IO, str]] = None,
base_url=None,
timeout=None,
timeout: Optional[int] = None,
verbose: Optional[bool] = False,
):
if (api_key is not None or api_secret is not None) and key_file is not None:
raise Exception(f"Cannot specify both api_key and key_file in constructor")
Expand Down
30 changes: 28 additions & 2 deletions coinbase/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
from coinbase.__version__ import __version__

API_ENV_KEY = "COINBASE_API_KEY"
API_SECRET_ENV_KEY = "COINBASE_API_SECRET"
USER_AGENT = f"coinbase-advanced-py/{__version__}"

# REST Constants
BASE_URL = "api.coinbase.com"
API_PREFIX = "/api/v3/brokerage"
REST_SERVICE = "retail_rest_api_proxy"

# Websocket Constants
WS_BASE_URL = "wss://advanced-trade-ws.coinbase.com"
WS_SERVICE = "public_websocket_api"
API_ENV_KEY = "COINBASE_API_KEY"
API_SECRET_ENV_KEY = "COINBASE_API_SECRET"

WS_RETRY_MAX = 5
WS_RETRY_BASE = 5
WS_RETRY_FACTOR = 1.5

# Message Types
SUBSCRIBE_MESSAGE_TYPE = "subscribe"
UNSUBSCRIBE_MESSAGE_TYPE = "unsubscribe"

# Channels
HEARTBEATS = "heartbeats"
CANDLES = "candles"
MARKET_TRADES = "market_trades"
STATUS = "status"
TICKER = "ticker"
TICKER_BATCH = "ticker_batch"
LEVEL2 = "level2"
USER = "user"
24 changes: 17 additions & 7 deletions coinbase/rest/rest_base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import json
import logging
import os
from typing import IO, Optional, Union

import requests
from requests.exceptions import HTTPError

from coinbase import jwt_generator
from coinbase.__version__ import __version__
from coinbase.api_base import APIBase
from coinbase.constants import API_ENV_KEY, API_SECRET_ENV_KEY, BASE_URL
from coinbase.api_base import APIBase, get_logger
from coinbase.constants import API_ENV_KEY, API_SECRET_ENV_KEY, BASE_URL, USER_AGENT

logger = get_logger("coinbase.RESTClient")


def handle_exception(response):
Expand All @@ -33,6 +34,7 @@ def handle_exception(response):
)

if http_error_msg:
logger.error(f"HTTP Error: {http_error_msg}")
raise HTTPError(http_error_msg, response=response)


Expand All @@ -43,15 +45,19 @@ def __init__(
api_secret: Optional[str] = os.getenv(API_SECRET_ENV_KEY),
key_file: Optional[Union[IO, str]] = None,
base_url=BASE_URL,
timeout=None,
timeout: Optional[int] = None,
verbose: Optional[bool] = False,
):
super().__init__(
api_key=api_key,
api_secret=api_secret,
key_file=key_file,
base_url=base_url,
timeout=timeout,
verbose=verbose,
)
if verbose:
logger.setLevel(logging.DEBUG)

def get(self, url_path, params: Optional[dict] = None, **kwargs):
params = params or {}
Expand Down Expand Up @@ -126,6 +132,8 @@ def send_request(self, http_method, url_path, params, headers, data=None):

url = f"https://{self.base_url}{url_path}"

logger.debug(f"Sending {http_method} request to {url}")

response = requests.request(
http_method,
url,
Expand All @@ -136,13 +144,15 @@ def send_request(self, http_method, url_path, params, headers, data=None):
)
handle_exception(response) # Raise an HTTPError for bad responses

logger.debug(f"Raw response: {response.json()}")

return response.json()

def set_headers(self, method, path):
uri = f"{method} {self.base_url}{path}"
jwt = jwt_generator.build_rest_jwt(uri, self.api_key, self.api_secret)
return {
"Content-Type": "application/json",
"Authorization": "Bearer " + jwt,
"User-Agent": "coinbase-advanced-py/" + __version__,
"Authorization": f"Bearer {jwt}",
"User-Agent": USER_AGENT,
}
38 changes: 38 additions & 0 deletions coinbase/websocket/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from .websocket_base import WSBase, WSClientConnectionClosedException, WSClientException


class WSClient(WSBase):
from .channels import (
candles,
candles_async,
candles_unsubscribe,
candles_unsubscribe_async,
heartbeats,
heartbeats_async,
heartbeats_unsubscribe,
heartbeats_unsubscribe_async,
level2,
level2_async,
level2_unsubscribe,
level2_unsubscribe_async,
market_trades,
market_trades_async,
market_trades_unsubscribe,
market_trades_unsubscribe_async,
status,
status_async,
status_unsubscribe,
status_unsubscribe_async,
ticker,
ticker_async,
ticker_batch,
ticker_batch_async,
ticker_batch_unsubscribe,
ticker_batch_unsubscribe_async,
ticker_unsubscribe,
ticker_unsubscribe_async,
user,
user_async,
user_unsubscribe,
user_unsubscribe_async,
)
Loading

0 comments on commit 36f0f02

Please sign in to comment.