Skip to content

Commit

Permalink
Merge pull request #228 from Davi0kProgramsThings/v3.0.0b3
Browse files Browse the repository at this point in the history
Merge branch `Davi0kProgramsThings:v3.0.0b3` into branch `bitfinexcom:master`.
  • Loading branch information
vigan-abd authored Nov 23, 2023
2 parents e281f7b + f63224c commit 90bba4e
Show file tree
Hide file tree
Showing 83 changed files with 1,766 additions and 1,697 deletions.
7 changes: 1 addition & 6 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,5 @@ A possible solution could be...

### Python version
<!-- Indicate your python version here -->
<!-- You can print it using `python3 --version`-->
<!-- You can print it using `python3 --version` -->
Python 3.10.6 x64

### Mypy version
<!-- Indicate your mypy version here -->
<!-- You can print it using `python3 -m mypy --version`-->
mypy 0.991 (compiled: yes)
3 changes: 0 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,5 @@ PR fixes the following issue:
- [ ] I have commented my code, particularly in hard-to-understand areas;
- [ ] I have made corresponding changes to the documentation;
- [ ] My changes generate no new warnings;
- [ ] I have added tests that prove my fix is effective or that my feature works;
- [ ] New and existing unit tests pass locally with my changes;
- [ ] Mypy returns no errors or warnings when run on the root package;
- [ ] Pylint returns a score of 10.00/10.00 when run on the root package;
- [ ] I have updated the library version and updated the CHANGELOG;
2 changes: 0 additions & 2 deletions .github/workflows/bitfinex-api-py-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,3 @@ jobs:
run: python -m pylint bfxapi
- name: Run mypy to check the correctness of type hinting (and fail if any error or warning is found)
run: python -m mypy bfxapi
- name: Execute project's unit tests (unittest)
run: python -m unittest bfxapi.tests
16 changes: 4 additions & 12 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,20 @@ py-version=3.8.0

[MESSAGES CONTROL]
disable=
multiple-imports,
missing-docstring,
logging-not-lazy,
logging-fstring-interpolation,
multiple-imports,
too-few-public-methods,
too-many-public-methods,
too-many-instance-attributes,
dangerous-default-value,
inconsistent-return-statements,

[SIMILARITIES]
min-similarity-lines=6
too-many-instance-attributes

[VARIABLES]
allowed-redefined-builtins=type,dir,id,all,format,len
allowed-redefined-builtins=all,dir,format,id,len,type

[FORMAT]
max-line-length=120
expected-line-ending-format=LF

[BASIC]
good-names=id,on,pl,t,ip,tf,A,B,C,D,E,F
good-names=f,t,id,ip,on,pl,tf,to,A,B,C,D,E,F

[TYPECHECK]
generated-members=websockets
Expand Down
11 changes: 0 additions & 11 deletions .travis.yml

This file was deleted.

51 changes: 4 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ _Revoke your API-KEYs and API-SECRETs immediately if you think they might have b

### Advanced features
* [Using custom notifications](#using-custom-notifications)
* [Setting up connection multiplexing](#setting-up-connection-multiplexing)

### Examples
* [Creating a new order](#creating-a-new-order)
Expand Down Expand Up @@ -181,10 +180,10 @@ A custom [close code number](https://www.iana.org/assignments/websocket/websocke
await bfx.wss.close(code=1001, reason="Going Away")
```

After closing the connection, the client will emit the `disconnection` event:
After closing the connection, the client will emit the `disconnected` event:
```python
@bfx.wss.on("disconnection")
def on_disconnection(code: int, reason: str):
@bfx.wss.on("disconnected")
def on_disconnected(code: int, reason: str):
if code == 1000 or code == 1001:
print("Closing the connection without errors!")
```
Expand All @@ -201,7 +200,7 @@ On each successful subscription, the client will emit the `subscribed` event:
@bfx.wss.on("subscribed")
def on_subscribed(subscription: subscriptions.Subscription):
if subscription["channel"] == "ticker":
print(f"{subscription['symbol']}: {subscription['subId']}") # tBTCUSD: f2757df2-7e11-4244-9bb7-a53b7343bef8
print(f"{subscription['symbol']}: {subscription['sub_id']}") # tBTCUSD: f2757df2-7e11-4244-9bb7-a53b7343bef8
```

### Unsubscribing from a public channel
Expand Down Expand Up @@ -242,11 +241,6 @@ The same can be done without using decorators:
bfx.wss.on("candles_update", callback=on_candles_update)
```

You can pass any number of events to register for the same callback function:
```python
bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update)
```

# Advanced features

## Using custom notifications
Expand All @@ -269,27 +263,6 @@ def on_notification(notification: Notification[Any]):
print(notification.data) # { "foo": 1 }
```

## Setting up connection multiplexing

`BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument:
```python
bfx.wss.run(connections=3)
```

`connections` indicates the number of connections to run concurrently (through connection multiplexing).

Each of these connections can handle up to 25 subscriptions to public channels. \
So, using `N` connections will allow the client to handle at most `N * 25` subscriptions. \
You should always use the minimum number of connections necessary to handle all the subscriptions that will be made.

For example, if you know that your application will subscribe to 75 public channels, 75 / 25 = 3 connections will be enough to handle all the subscriptions.

The default number of connections is 5; therefore, if the `connections` argument is not given, the client will be able to handle a maximum of 25 * 5 = 125 subscriptions.

Keep in mind that using a large number of connections could slow down the client performance.

The use of more than 20 connections is not recommended.

# Examples

## Creating a new order
Expand Down Expand Up @@ -340,7 +313,6 @@ Contributors must uphold the [Contributor Covenant code of conduct](https://gith
* [Cloning the repository](#cloning-the-repository)
* [Installing the dependencies](#installing-the-dependencies)
2. [Before opening a PR](#before-opening-a-pr)
* [Running the unit tests](#running-the-unit-tests)
3. [License](#license)

## Installation and setup
Expand Down Expand Up @@ -376,24 +348,9 @@ Wheter you're submitting a bug fix, a new feature or a documentation change, you
All PRs must follow this [PULL_REQUEST_TEMPLATE](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/.github/PULL_REQUEST_TEMPLATE.md) and include an exhaustive description.

Before opening a pull request, you should also make sure that:
- [ ] all unit tests pass (see [Running the unit tests](#running-the-unit-tests)).
- [ ] [`pylint`](https://github.com/pylint-dev/pylint) returns a score of 10.00/10.00 when run against your code.
- [ ] [`mypy`](https://github.com/python/mypy) doesn't throw any error code when run on the project (excluding notes).

### Running the unit tests

`bitfinex-api-py` comes with a set of unit tests (written using the [`unittest`](https://docs.python.org/3.8/library/unittest.html) unit testing framework). \
Contributors must ensure that each unit test passes before opening a pull request. \
You can run all project's unit tests by calling `unittest` on `bfxapi.tests`:
```console
python3 -m unittest -v bfxapi.tests
```

A single unit test can be run as follows:
```console
python3 -m unittest -v bfxapi.tests.test_notification
```

## License

```
Expand Down
12 changes: 6 additions & 6 deletions bfxapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .client import Client

from .urls import REST_HOST, PUB_REST_HOST, \
WSS_HOST, PUB_WSS_HOST

from .version import __version__
from ._client import \
Client, \
REST_HOST, \
WSS_HOST, \
PUB_REST_HOST, \
PUB_WSS_HOST
52 changes: 52 additions & 0 deletions bfxapi/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import \
TYPE_CHECKING, List, Optional

from bfxapi._utils.logging import ColorLogger

from bfxapi.rest import BfxRestInterface
from bfxapi.websocket import BfxWebSocketClient
from bfxapi.exceptions import IncompleteCredentialError

if TYPE_CHECKING:
from bfxapi.websocket._client.bfx_websocket_client import \
_Credentials

REST_HOST = "https://api.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2"

PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"

class Client:
def __init__(
self,
api_key: Optional[str] = None,
api_secret: Optional[str] = None,
*,
rest_host: str = REST_HOST,
wss_host: str = WSS_HOST,
filters: Optional[List[str]] = None,
timeout: Optional[int] = 60 * 15,
log_filename: Optional[str] = None
) -> None:
credentials: Optional["_Credentials"] = None

if api_key and api_secret:
credentials = \
{ "api_key": api_key, "api_secret": api_secret, "filters": filters }
elif api_key:
raise IncompleteCredentialError( \
"You must provide both an API-KEY and an API-SECRET (missing API-KEY).")
elif api_secret:
raise IncompleteCredentialError( \
"You must provide both an API-KEY and an API-SECRET (missing API-SECRET).")

self.rest = BfxRestInterface(rest_host, api_key, api_secret)

logger = ColorLogger("bfxapi", level="INFO")

if log_filename:
logger.register(filename=log_filename)

self.wss = BfxWebSocketClient(wss_host, \
credentials=credentials, timeout=timeout, logger=logger)
File renamed without changes.
13 changes: 13 additions & 0 deletions bfxapi/_utils/json_decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Dict, Any

import re, json

def _to_snake_case(string: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", string).lower()

def _object_hook(data: Dict[str, Any]) -> Any:
return { _to_snake_case(key): value for key, value in data.items() }

class JSONDecoder(json.JSONDecoder):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(object_hook=_object_hook, *args, **kwargs)
36 changes: 36 additions & 0 deletions bfxapi/_utils/json_encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import \
Union, List, Dict, \
Any

import json

from decimal import Decimal

_ExtJSON = Union[Dict[str, "_ExtJSON"], List["_ExtJSON"], \
bool, int, float, str, Decimal, None]

_StrictJSON = Union[Dict[str, "_StrictJSON"], List["_StrictJSON"], \
int, str, None]

def _clear(dictionary: Dict[str, Any]) -> Dict[str, Any]:
return { key: value for key, value in dictionary.items() \
if value is not None }

def _adapter(data: _ExtJSON) -> _StrictJSON:
if isinstance(data, bool):
return int(data)
if isinstance(data, float):
return format(Decimal(repr(data)), "f")
if isinstance(data, Decimal):
return format(data, "f")

if isinstance(data, list):
return [ _adapter(sub_data) for sub_data in data ]
if isinstance(data, dict):
return _clear({ key: _adapter(value) for key, value in data.items() })

return data

class JSONEncoder(json.JSONEncoder):
def encode(self, o: _ExtJSON) -> str:
return super().encode(_adapter(o))
67 changes: 67 additions & 0 deletions bfxapi/_utils/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import \
TYPE_CHECKING, Literal, Optional

#pylint: disable-next=wildcard-import,unused-wildcard-import
from logging import *

from copy import copy

import sys

if TYPE_CHECKING:
_Level = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

_BLACK, _RED, _GREEN, _YELLOW, \
_BLUE, _MAGENTA, _CYAN, _WHITE = \
[ f"\033[0;{90 + i}m" for i in range(8) ]

_BOLD_BLACK, _BOLD_RED, _BOLD_GREEN, _BOLD_YELLOW, \
_BOLD_BLUE, _BOLD_MAGENTA, _BOLD_CYAN, _BOLD_WHITE = \
[ f"\033[1;{90 + i}m" for i in range(8) ]

_NC = "\033[0m"

class _ColorFormatter(Formatter):
__LEVELS = {
"INFO": _BLUE,
"WARNING": _YELLOW,
"ERROR": _RED,
"CRITICAL": _BOLD_RED,
"DEBUG": _BOLD_WHITE
}

def format(self, record: LogRecord) -> str:
_record = copy(record)
_record.name = _MAGENTA + record.name + _NC
_record.levelname = _ColorFormatter.__format_level(record.levelname)

return super().format(_record)

#pylint: disable-next=invalid-name
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
return _GREEN + super().formatTime(record, datefmt) + _NC

@staticmethod
def __format_level(level: str) -> str:
return _ColorFormatter.__LEVELS[level] + level + _NC

_FORMAT = "%(asctime)s %(name)s %(levelname)s %(message)s"

_DATE_FORMAT = "%d-%m-%Y %H:%M:%S"

class ColorLogger(Logger):
__FORMATTER = Formatter(_FORMAT,_DATE_FORMAT)

def __init__(self, name: str, level: "_Level" = "NOTSET") -> None:
super().__init__(name, level)

formatter = _ColorFormatter(_FORMAT, _DATE_FORMAT)

handler = StreamHandler(stream=sys.stderr)
handler.setFormatter(fmt=formatter)
self.addHandler(hdlr=handler)

def register(self, filename: str) -> None:
handler = FileHandler(filename=filename)
handler.setFormatter(fmt=ColorLogger.__FORMATTER)
self.addHandler(hdlr=handler)
1 change: 1 addition & 0 deletions bfxapi/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "3.0.0b3"
Loading

0 comments on commit 90bba4e

Please sign in to comment.