From 65956846ad3c08f941318716ff60bf369e4c7cb6 Mon Sep 17 00:00:00 2001 From: Alex Contryman Date: Thu, 7 Jan 2021 19:55:13 -0800 Subject: [PATCH 1/2] Add pre-commit and black --- .gitignore | 2 +- .pre-commit-config.yaml | 11 + AUTHORS.md | 4 +- HISTORY.rst | 4 +- LICENSE | 2 +- Makefile | 2 +- README.rst | 4 +- coinbasepro/.DS_Store | Bin 0 -> 6148 bytes coinbasepro/auth.py | 22 +- coinbasepro/auth_client.py | 614 +++++++++++++++++++---------------- coinbasepro/exceptions.py | 2 - coinbasepro/public_client.py | 230 +++++++------ coinbasepro/rate_limiter.py | 7 +- coinbasepro/token_bucket.py | 16 +- requirements.txt | 15 +- setup.py | 46 ++- 16 files changed, 530 insertions(+), 451 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 coinbasepro/.DS_Store diff --git a/.gitignore b/.gitignore index 43cae6e..e854bba 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ build/ dist coinbasepro.egg-info/ -__pycache__/ \ No newline at end of file +__pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..79e10ac --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black diff --git a/AUTHORS.md b/AUTHORS.md index 3635df4..ba000c4 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,7 +3,7 @@ * khoo-j ## Acknowledgements -This package was derived from: +This package was derived from: ###GDAX-Python @@ -25,4 +25,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/HISTORY.rst b/HISTORY.rst index c865041..c543319 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,7 +20,7 @@ dev **Improvements** -- Added rate-limiting to all public and authenticated endpoints. Dropping support for Python 3.4 to keep the implemenation simple. +- Added rate-limiting to all public and authenticated endpoints. Dropping support for Python 3.4 to keep the implementation simple. 0.1.1 (2019-07-23) ++++++++++++++++++ @@ -101,4 +101,4 @@ dev 0.0.1 (2018-06-27) +++++++++++++++++++ -- Hello world. \ No newline at end of file +- Hello world. diff --git a/LICENSE b/LICENSE index 0dcda18..2ccd933 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/Makefile b/Makefile index 9b69218..d96ad6c 100644 --- a/Makefile +++ b/Makefile @@ -8,4 +8,4 @@ publish: twine upload dist/* rm -rf build dist .egg coinbasepro.egg-info -.PHONY: init publish \ No newline at end of file +.PHONY: init publish diff --git a/README.rst b/README.rst index 8f35352..5114540 100644 --- a/README.rst +++ b/README.rst @@ -105,7 +105,7 @@ Features # This call throws a BadRequest exception >>> auth_client.get_order('invalid_order_num') coinbasepro.exceptions.BadRequest: Invalid order id - + # CoinbaseAPIError is the parent exception for all exceptions the API # throws, so catching this will catch anything >>> try: @@ -120,4 +120,4 @@ Installation .. code-block:: bash - $ pip install coinbasepro \ No newline at end of file + $ pip install coinbasepro diff --git a/coinbasepro/.DS_Store b/coinbasepro/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 0: - self.a_rate_limiter = RateLimiter(burst_size=auth_burst_size, - rate_limit=auth_rate_limit) + self.a_rate_limiter = RateLimiter( + burst_size=auth_burst_size, rate_limit=auth_rate_limit + ) else: self.a_rate_limiter = None @@ -107,10 +110,11 @@ def get_accounts(self) -> List[Dict[str, Any]]: See `get_products()`. """ - return self._get_account_helper('') + return self._get_account_helper("") def get_account_history( - self, account_id: str, **kwargs) -> Iterator[Dict[str, Any]]: + self, account_id: str, **kwargs + ) -> Iterator[Dict[str, Any]]: """List account activity. Account activity either increases or decreases your account @@ -155,18 +159,18 @@ def get_account_history( See `get_products()`. """ - field_conversions = {'created_at': self._parse_datetime, - 'amount': Decimal, - 'balance': Decimal} - endpoint = '/accounts/{}/ledger'.format(account_id) - r = self._send_paginated_message(endpoint, - params=kwargs, - rate_limiter=self.a_rate_limiter) - return (self._convert_dict(activity, field_conversions) - for activity in r) - - def get_account_holds( - self, account_id: str, **kwargs) -> Iterator[Dict[str, Any]]: + field_conversions = { + "created_at": self._parse_datetime, + "amount": Decimal, + "balance": Decimal, + } + endpoint = "/accounts/{}/ledger".format(account_id) + r = self._send_paginated_message( + endpoint, params=kwargs, rate_limiter=self.a_rate_limiter + ) + return (self._convert_dict(activity, field_conversions) for activity in r) + + def get_account_holds(self, account_id: str, **kwargs) -> Iterator[Dict[str, Any]]: """Get holds on an account. Holds are placed on an account for active orders or @@ -208,24 +212,28 @@ def get_account_holds( See `get_products()`. """ - field_conversions = {'created_at': self._parse_datetime, - 'updated_at': self._parse_datetime, - 'amount': Decimal} - endpoint = '/accounts/{}/holds'.format(account_id) - r = self._send_paginated_message(endpoint, - params=kwargs, - rate_limiter=self.a_rate_limiter) + field_conversions = { + "created_at": self._parse_datetime, + "updated_at": self._parse_datetime, + "amount": Decimal, + } + endpoint = "/accounts/{}/holds".format(account_id) + r = self._send_paginated_message( + endpoint, params=kwargs, rate_limiter=self.a_rate_limiter + ) return (self._convert_dict(hold, field_conversions) for hold in r) - def place_order(self, - product_id: str, - side: str, - order_type: str, - stop: Optional[str] = None, - stop_price: Optional[Union[float, Decimal]] = None, - client_oid: Optional[str] = None, - stp: Optional[str] = None, - **kwargs) -> Dict[str, Any]: + def place_order( + self, + product_id: str, + side: str, + order_type: str, + stop: Optional[str] = None, + stop_price: Optional[Union[float, Decimal]] = None, + client_oid: Optional[str] = None, + stp: Optional[str] = None, + **kwargs + ) -> Dict[str, Any]: """Place an order. The two order types (limit and market) can be placed using this @@ -284,60 +292,72 @@ def place_order(self, """ # Market order checks - if order_type == 'market': - if kwargs.get('size') is None and kwargs.get('funds') is None: - raise ValueError('Must specify `size` or `funds` for a market ' - 'order') + if order_type == "market": + if kwargs.get("size") is None and kwargs.get("funds") is None: + raise ValueError("Must specify `size` or `funds` for a market " "order") # Limit order checks - if order_type == 'limit': - if (kwargs.get('cancel_after') is not None and - kwargs.get('time_in_force') != 'GTT'): - raise ValueError('May only specify a cancel period when time ' - 'in_force is `GTT`') - if (kwargs.get('post_only') is not None and - kwargs.get('time_in_force') in ['IOC', 'FOK']): - raise ValueError('post_only is invalid when time in force is ' - '`IOC` or `FOK`') + if order_type == "limit": + if ( + kwargs.get("cancel_after") is not None + and kwargs.get("time_in_force") != "GTT" + ): + raise ValueError( + "May only specify a cancel period when time " "in_force is `GTT`" + ) + if kwargs.get("post_only") is not None and kwargs.get("time_in_force") in [ + "IOC", + "FOK", + ]: + raise ValueError( + "post_only is invalid when time in force is " "`IOC` or `FOK`" + ) # Stop order checks if (stop is not None) ^ (stop_price is not None): - raise ValueError('Both `stop` and `stop_price` must be specified at' - 'the same time.') + raise ValueError( + "Both `stop` and `stop_price` must be specified at" "the same time." + ) # Build params dict - params = {'product_id': product_id, - 'side': side, - 'type': order_type, - 'stop': stop, - 'stop_price': stop_price, - 'client_oid': client_oid, - 'stp': stp} + params = { + "product_id": product_id, + "side": side, + "type": order_type, + "stop": stop, + "stop_price": stop_price, + "client_oid": client_oid, + "stp": stp, + } params.update(kwargs) - field_conversions = {'price': Decimal, - 'size': Decimal, - 'created_at': self._parse_datetime, - 'fill_fees': Decimal, - 'filled_size': Decimal, - 'executed_value': Decimal} - r = self._send_message('post', '/orders', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) + field_conversions = { + "price": Decimal, + "size": Decimal, + "created_at": self._parse_datetime, + "fill_fees": Decimal, + "filled_size": Decimal, + "executed_value": Decimal, + } + r = self._send_message( + "post", "/orders", data=json.dumps(params), rate_limiter=self.a_rate_limiter + ) return self._convert_dict(r, field_conversions) - def place_limit_order(self, - product_id: str, - side: str, - price: Union[float, Decimal], - size: Union[float, Decimal], - stop: Optional[str] = None, - stop_price: Optional[Union[float, Decimal]] = None, - client_oid: Optional[str] = None, - stp: Optional[str] = None, - time_in_force: Optional[str] = None, - cancel_after: Optional[str] = None, - post_only: Optional[bool] = None) -> Dict[str, Any]: + def place_limit_order( + self, + product_id: str, + side: str, + price: Union[float, Decimal], + size: Union[float, Decimal], + stop: Optional[str] = None, + stop_price: Optional[Union[float, Decimal]] = None, + client_oid: Optional[str] = None, + stp: Optional[str] = None, + time_in_force: Optional[str] = None, + cancel_after: Optional[str] = None, + post_only: Optional[bool] = None, + ) -> Dict[str, Any]: """Place a limit order. Args: @@ -376,31 +396,35 @@ def place_limit_order(self, See `get_products()`. """ - params = {'product_id': product_id, - 'side': side, - 'order_type': 'limit', - 'price': price, - 'size': size, - 'stop': stop, - 'stop_price': stop_price, - 'client_oid': client_oid, - 'stp': stp, - 'time_in_force': time_in_force, - 'cancel_after': cancel_after, - 'post_only': post_only} + params = { + "product_id": product_id, + "side": side, + "order_type": "limit", + "price": price, + "size": size, + "stop": stop, + "stop_price": stop_price, + "client_oid": client_oid, + "stp": stp, + "time_in_force": time_in_force, + "cancel_after": cancel_after, + "post_only": post_only, + } params = dict((k, v) for k, v in params.items() if v is not None) return self.place_order(**params) - def place_market_order(self, - product_id: str, - side: str, - size: Union[float, Decimal] = None, - funds: Union[float, Decimal] = None, - stop: Optional[str] = None, - stop_price: Optional[Union[float, Decimal]] = None, - client_oid: Optional[str] = None, - stp: Optional[str] = None) -> Dict[str, Any]: + def place_market_order( + self, + product_id: str, + side: str, + size: Union[float, Decimal] = None, + funds: Union[float, Decimal] = None, + stop: Optional[str] = None, + stop_price: Optional[Union[float, Decimal]] = None, + client_oid: Optional[str] = None, + stp: Optional[str] = None, + ) -> Dict[str, Any]: """Place a market order. `size` and `funds` parameters specify the order amount. `funds` @@ -435,15 +459,17 @@ def place_market_order(self, See `get_products()`. """ - params = {'product_id': product_id, - 'side': side, - 'order_type': 'market', - 'size': size, - 'funds': funds, - 'stop': stop, - 'stop_price': stop_price, - 'client_oid': client_oid, - 'stp': stp} + params = { + "product_id": product_id, + "side": side, + "order_type": "market", + "size": size, + "funds": funds, + "stop": stop, + "stop_price": stop_price, + "client_oid": client_oid, + "stp": stp, + } params = dict((k, v) for k, v in params.items() if v is not None) return self.place_order(**params) @@ -473,9 +499,9 @@ def cancel_order(self, order_id: str) -> List[str]: See `get_products()`. """ - return self._send_message('delete', - '/orders/' + order_id, - rate_limiter=self.a_rate_limiter) + return self._send_message( + "delete", "/orders/" + order_id, rate_limiter=self.a_rate_limiter + ) def cancel_all(self, product_id: Optional[str] = None) -> List[str]: """With best effort, cancel all open orders. @@ -498,13 +524,12 @@ def cancel_all(self, product_id: Optional[str] = None) -> List[str]: """ if product_id is not None: - params = {'product_id': product_id} + params = {"product_id": product_id} else: params = None - return self._send_message('delete', - '/orders', - params=params, - rate_limiter=self.a_rate_limiter) + return self._send_message( + "delete", "/orders", params=params, rate_limiter=self.a_rate_limiter + ) def get_order(self, order_id: str) -> Dict[str, Any]: """Get a single order by order id. @@ -542,21 +567,25 @@ def get_order(self, order_id: str) -> Dict[str, Any]: See `get_products()`. """ - field_conversions = {'created_at': self._parse_datetime, - 'executed_value': Decimal, - 'fill_fees': Decimal, - 'filled_size': Decimal, - 'price': Decimal, - 'size': Decimal} - r = self._send_message('get', - '/orders/' + order_id, - rate_limiter=self.a_rate_limiter) + field_conversions = { + "created_at": self._parse_datetime, + "executed_value": Decimal, + "fill_fees": Decimal, + "filled_size": Decimal, + "price": Decimal, + "size": Decimal, + } + r = self._send_message( + "get", "/orders/" + order_id, rate_limiter=self.a_rate_limiter + ) return self._convert_dict(r, field_conversions) - def get_orders(self, - product_id: Optional[str] = None, - status: Optional[Union[str, List[str]]] = None, - **kwargs) -> Iterator[Dict[str, Any]]: + def get_orders( + self, + product_id: Optional[str] = None, + status: Optional[Union[str, List[str]]] = None, + **kwargs + ) -> Iterator[Dict[str, Any]]: """List your current open orders. Only open or un-settled orders are returned. As soon as an @@ -614,26 +643,26 @@ def get_orders(self, """ params = kwargs if product_id is not None: - params['product_id'] = product_id + params["product_id"] = product_id if status is not None: - params['status'] = status - - field_conversions = {'price': Decimal, - 'size': Decimal, - 'created_at': self._parse_datetime, - 'fill_fees': Decimal, - 'filled_size': Decimal, - 'executed_value': Decimal} - orders = self._send_paginated_message('/orders', - params=params, - rate_limiter=self.a_rate_limiter) - return (self._convert_dict(order, field_conversions) - for order in orders) - - def get_fills(self, - product_id: Optional[str] = None, - order_id: Optional[str] = None, - **kwargs) -> Iterator[Dict[str, Any]]: + params["status"] = status + + field_conversions = { + "price": Decimal, + "size": Decimal, + "created_at": self._parse_datetime, + "fill_fees": Decimal, + "filled_size": Decimal, + "executed_value": Decimal, + } + orders = self._send_paginated_message( + "/orders", params=params, rate_limiter=self.a_rate_limiter + ) + return (self._convert_dict(order, field_conversions) for order in orders) + + def get_fills( + self, product_id: Optional[str] = None, order_id: Optional[str] = None, **kwargs + ) -> Iterator[Dict[str, Any]]: """Get recent fills for a product or order. Either `product_id` or `order_id` must be specified. @@ -680,35 +709,39 @@ def get_fills(self, """ if (product_id is None) and (order_id is None): - raise ValueError('Either product_id or order_id must be specified.') + raise ValueError("Either product_id or order_id must be specified.") params = {} if product_id: - params['product_id'] = product_id + params["product_id"] = product_id if order_id: - params['order_id'] = order_id + params["order_id"] = order_id params.update(kwargs) - field_conversions = {'price': Decimal, - 'size': Decimal, - 'created_at': self._parse_datetime, - 'fee': Decimal} + field_conversions = { + "price": Decimal, + "size": Decimal, + "created_at": self._parse_datetime, + "fee": Decimal, + } def convert_volume_keys(fill): """Convert any 'volume' keys (like 'usd_volume') to Decimal.""" for k, v in fill.items(): - if 'volume' in k and v is not None: + if "volume" in k and v is not None: fill[k] = Decimal(fill[k]) return fill - fills = self._send_paginated_message('/fills', - params=params, - rate_limiter=self.a_rate_limiter) - return (self._convert_dict(convert_volume_keys(fill), field_conversions) - for fill in fills) - - def deposit(self, - amount: Union[float, Decimal], - currency: str, - payment_method_id: str) -> Dict[str, Any]: + + fills = self._send_paginated_message( + "/fills", params=params, rate_limiter=self.a_rate_limiter + ) + return ( + self._convert_dict(convert_volume_keys(fill), field_conversions) + for fill in fills + ) + + def deposit( + self, amount: Union[float, Decimal], currency: str, payment_method_id: str + ) -> Dict[str, Any]: """Deposit funds from a payment method. See AuthenticatedClient.get_payment_methods() to receive @@ -732,21 +765,23 @@ def deposit(self, See `get_products()`. """ - params = {'amount': amount, - 'currency': currency, - 'payment_method_id': payment_method_id} - field_conversions = {'amount': Decimal, - 'payout_at': self._parse_datetime} - r = self._send_message('post', - '/deposits/payment-method', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) + params = { + "amount": amount, + "currency": currency, + "payment_method_id": payment_method_id, + } + field_conversions = {"amount": Decimal, "payout_at": self._parse_datetime} + r = self._send_message( + "post", + "/deposits/payment-method", + data=json.dumps(params), + rate_limiter=self.a_rate_limiter, + ) return self._convert_dict(r, field_conversions) - def deposit_from_coinbase(self, - amount: Union[float, Decimal], - currency: str, - coinbase_account_id: str) -> Dict[str, Any]: + def deposit_from_coinbase( + self, amount: Union[float, Decimal], currency: str, coinbase_account_id: str + ) -> Dict[str, Any]: """Deposit funds from a Coinbase account. You can move funds between your Coinbase accounts and your @@ -773,19 +808,22 @@ def deposit_from_coinbase(self, See `get_products()`. """ - params = {'amount': amount, - 'currency': currency, - 'coinbase_account_id': coinbase_account_id} - r = self._send_message('post', - '/deposits/coinbase-account', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) - return self._convert_dict(r, {'amount': Decimal}) - - def withdraw(self, - amount: Union[float, Decimal], - currency: str, - payment_method_id: str) -> Dict[str, Any]: + params = { + "amount": amount, + "currency": currency, + "coinbase_account_id": coinbase_account_id, + } + r = self._send_message( + "post", + "/deposits/coinbase-account", + data=json.dumps(params), + rate_limiter=self.a_rate_limiter, + ) + return self._convert_dict(r, {"amount": Decimal}) + + def withdraw( + self, amount: Union[float, Decimal], currency: str, payment_method_id: str + ) -> Dict[str, Any]: """Withdraw funds to a payment method. See AuthenticatedClient.get_payment_methods() to receive @@ -809,21 +847,23 @@ def withdraw(self, See `get_products()`. """ - params = {'amount': amount, - 'currency': currency, - 'payment_method_id': payment_method_id} - field_conversions = {'amount': Decimal, - 'payout_at': self._parse_datetime} - r = self._send_message('post', - '/withdrawals/payment-method', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) + params = { + "amount": amount, + "currency": currency, + "payment_method_id": payment_method_id, + } + field_conversions = {"amount": Decimal, "payout_at": self._parse_datetime} + r = self._send_message( + "post", + "/withdrawals/payment-method", + data=json.dumps(params), + rate_limiter=self.a_rate_limiter, + ) return self._convert_dict(r, field_conversions) - def withdraw_to_coinbase(self, - amount: Union[float, Decimal], - currency: str, - coinbase_account_id: str) -> Dict[str, Any]: + def withdraw_to_coinbase( + self, amount: Union[float, Decimal], currency: str, coinbase_account_id: str + ) -> Dict[str, Any]: """Withdraw funds to a coinbase account. You can move funds between your Coinbase accounts and your @@ -850,19 +890,22 @@ def withdraw_to_coinbase(self, See `get_products()`. """ - params = {'amount': amount, - 'currency': currency, - 'coinbase_account_id': coinbase_account_id} - r = self._send_message('post', - '/withdrawals/coinbase', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) - return self._convert_dict(r, {'amount': Decimal}) - - def withdraw_to_crypto(self, - amount: Union[float, Decimal], - currency: str, - crypto_address: str): + params = { + "amount": amount, + "currency": currency, + "coinbase_account_id": coinbase_account_id, + } + r = self._send_message( + "post", + "/withdrawals/coinbase", + data=json.dumps(params), + rate_limiter=self.a_rate_limiter, + ) + return self._convert_dict(r, {"amount": Decimal}) + + def withdraw_to_crypto( + self, amount: Union[float, Decimal], currency: str, crypto_address: str + ): """Withdraw funds to a crypto address. Args: @@ -882,14 +925,18 @@ def withdraw_to_crypto(self, See `get_products()`. """ - params = {'amount': amount, - 'currency': currency, - 'crypto_address': crypto_address} - r = self._send_message('post', - '/withdrawals/crypto', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) - return self._convert_dict(r, {'amount': Decimal}) + params = { + "amount": amount, + "currency": currency, + "crypto_address": crypto_address, + } + r = self._send_message( + "post", + "/withdrawals/crypto", + data=json.dumps(params), + rate_limiter=self.a_rate_limiter, + ) + return self._convert_dict(r, {"amount": Decimal}) def get_payment_methods(self) -> List[Dict[str, Any]]: """Get a list of your payment methods. @@ -901,11 +948,13 @@ def get_payment_methods(self) -> List[Dict[str, Any]]: See `get_products()`. """ - field_conversions = {'created_at': self._parse_datetime, - 'updated_at': self._parse_datetime} - r = self._send_message('get', - '/payment-methods', - rate_limiter=self.a_rate_limiter) + field_conversions = { + "created_at": self._parse_datetime, + "updated_at": self._parse_datetime, + } + r = self._send_message( + "get", "/payment-methods", rate_limiter=self.a_rate_limiter + ) return self._convert_list_of_dicts(r, field_conversions) def get_coinbase_accounts(self) -> List[Dict[str, Any]]: @@ -918,21 +967,20 @@ def get_coinbase_accounts(self) -> List[Dict[str, Any]]: See `get_products()`. """ - field_conversions = {'balance': Decimal, - 'hold_balance': Decimal} - r = self._send_message('get', - '/coinbase-accounts', - self.a_rate_limiter) + field_conversions = {"balance": Decimal, "hold_balance": Decimal} + r = self._send_message("get", "/coinbase-accounts", self.a_rate_limiter) return self._convert_list_of_dicts(r, field_conversions) - def create_report(self, - report_type: str, - start_date: str, - end_date: str, - product_id: Optional[str] = None, - account_id: Optional[str] = None, - report_format: str = 'pdf', - email: Optional[str] = None) -> Dict[str, Any]: + def create_report( + self, + report_type: str, + start_date: str, + end_date: str, + product_id: Optional[str] = None, + account_id: Optional[str] = None, + report_format: str = "pdf", + email: Optional[str] = None, + ) -> Dict[str, Any]: """Create report of historic information about your account. The report will be generated when resources are available. @@ -969,21 +1017,25 @@ def create_report(self, See `get_products()`. """ - params = {'type': report_type, - 'start_date': start_date, - 'end_date': end_date, - 'format': report_format} + params = { + "type": report_type, + "start_date": start_date, + "end_date": end_date, + "format": report_format, + } if product_id is not None: - params['product_id'] = product_id + params["product_id"] = product_id if account_id is not None: - params['account_id'] = account_id + params["account_id"] = account_id if email is not None: - params['email'] = email + params["email"] = email - return self._send_message('post', - '/reports', - data=json.dumps(params), - rate_limiter=self.a_rate_limiter) + return self._send_message( + "post", + "/reports", + data=json.dumps(params), + rate_limiter=self.a_rate_limiter, + ) def get_report(self, report_id: str) -> Dict[str, Any]: """Get report status. @@ -1000,9 +1052,9 @@ def get_report(self, report_id: str) -> Dict[str, Any]: See `get_products()`. """ - return self._send_message('get', - '/reports/' + report_id, - rate_limiter=self.a_rate_limiter) + return self._send_message( + "get", "/reports/" + report_id, rate_limiter=self.a_rate_limiter + ) def get_trailing_volume(self) -> List[Dict[str, Any]]: """Get your 30-day trailing volume for all products. @@ -1027,21 +1079,21 @@ def get_trailing_volume(self) -> List[Dict[str, Any]]: See `get_products()`. """ - field_conversions = {'exchange_volume': Decimal, - 'volume': Decimal, - 'recorded_at': self._parse_datetime} - r = self._send_message('get', - '/users/self/trailing-volume', - rate_limiter=self.a_rate_limiter) + field_conversions = { + "exchange_volume": Decimal, + "volume": Decimal, + "recorded_at": self._parse_datetime, + } + r = self._send_message( + "get", "/users/self/trailing-volume", rate_limiter=self.a_rate_limiter + ) return self._convert_dict(field_conversions, r) def _get_account_helper(self, account_id): - field_conversions = {'balance': Decimal, - 'available': Decimal, - 'hold': Decimal} - r = self._send_message('get', - '/accounts/' + account_id, - rate_limiter=self.a_rate_limiter) + field_conversions = {"balance": Decimal, "available": Decimal, "hold": Decimal} + r = self._send_message( + "get", "/accounts/" + account_id, rate_limiter=self.a_rate_limiter + ) # Need to handle empty string `account_id`, which returns all accounts if type(r) is list: return self._convert_list_of_dicts(r, field_conversions) diff --git a/coinbasepro/exceptions.py b/coinbasepro/exceptions.py index 358d83d..6939e09 100644 --- a/coinbasepro/exceptions.py +++ b/coinbasepro/exceptions.py @@ -1,5 +1,3 @@ - - class CoinbaseAPIError(IOError): """There was an ambiguous exception that occurred.""" diff --git a/coinbasepro/public_client.py b/coinbasepro/public_client.py index f976e5c..92b5361 100644 --- a/coinbasepro/public_client.py +++ b/coinbasepro/public_client.py @@ -3,8 +3,13 @@ from decimal import Decimal from typing import Any, Dict, Iterator, List, Optional, Union -from coinbasepro.exceptions import (CoinbaseAPIError, BadRequest, InvalidAPIKey, - InvalidAuthorization, RateLimitError) +from coinbasepro.exceptions import ( + CoinbaseAPIError, + BadRequest, + InvalidAPIKey, + InvalidAuthorization, + RateLimitError, +) from coinbasepro.rate_limiter import RateLimiter @@ -18,11 +23,13 @@ class PublicClient(object): """ - def __init__(self, - api_url: str = 'https://api.pro.coinbase.com', - request_timeout: int = 30, - rate_limit: int = 3, - burst_size: int = 6): + def __init__( + self, + api_url: str = "https://api.pro.coinbase.com", + request_timeout: int = 30, + rate_limit: int = 3, + burst_size: int = 6, + ): """Create a Coinbase Pro API public client instance. Args: @@ -34,13 +41,14 @@ def __init__(self, when rate-limiting is enabled. """ - self.url = api_url.rstrip('/') + self.url = api_url.rstrip("/") self.auth = None # No auth needed for public client self.session = requests.Session() self.request_timeout = request_timeout if rate_limit > 0: - self.p_rate_limiter = RateLimiter(burst_size=burst_size, - rate_limit=rate_limit) + self.p_rate_limiter = RateLimiter( + burst_size=burst_size, rate_limit=rate_limit + ) else: self.p_rate_limiter = None @@ -92,12 +100,12 @@ def get_products(self) -> List[Dict[str, Any]]: Additionally, parent of all above exceptions. """ - field_conversions = {'base_min_size': Decimal, - 'base_max_size': Decimal, - 'quote_increment': Decimal} - r = self._send_message('get', - '/products', - rate_limiter=self.p_rate_limiter) + field_conversions = { + "base_min_size": Decimal, + "base_max_size": Decimal, + "quote_increment": Decimal, + } + r = self._send_message("get", "/products", rate_limiter=self.p_rate_limiter) return self._convert_list_of_dicts(r, field_conversions) def get_product_order_book(self, product_id: str, level: int = 1) -> Dict: @@ -137,11 +145,13 @@ def get_product_order_book(self, product_id: str, level: int = 1) -> Dict: See `get_products()`. """ - params = {'level': level} - return self._send_message('get', - '/products/{}/book'.format(product_id), - params=params, - rate_limiter=self.p_rate_limiter) + params = {"level": level} + return self._send_message( + "get", + "/products/{}/book".format(product_id), + params=params, + rate_limiter=self.p_rate_limiter, + ) def get_product_ticker(self, product_id: str) -> Dict[str, Any]: """Snapshot about the last trade (tick), best bid/ask and 24h volume. @@ -168,16 +178,20 @@ def get_product_ticker(self, product_id: str) -> Dict[str, Any]: See `get_products()`. """ - field_conversions = {'trade_id': int, - 'price': Decimal, - 'size': Decimal, - 'bid': Decimal, - 'ask': Decimal, - 'volume': Decimal, - 'time': self._parse_datetime} - r = self._send_message('get', - '/products/{}/ticker'.format(product_id), - rate_limiter=self.p_rate_limiter) + field_conversions = { + "trade_id": int, + "price": Decimal, + "size": Decimal, + "bid": Decimal, + "ask": Decimal, + "volume": Decimal, + "time": self._parse_datetime, + } + r = self._send_message( + "get", + "/products/{}/ticker".format(product_id), + rate_limiter=self.p_rate_limiter, + ) return self._convert_dict(r, field_conversions) def get_product_trades(self, product_id: str) -> Iterator[Dict[str, Any]]: @@ -209,23 +223,24 @@ def get_product_trades(self, product_id: str) -> Iterator[Dict[str, Any]]: See `get_products()`. """ - field_conversions = {'time': self._parse_datetime, - 'trade_id': int, - 'price': Decimal, - 'size': Decimal} + field_conversions = { + "time": self._parse_datetime, + "trade_id": int, + "price": Decimal, + "size": Decimal, + } trades = self._send_paginated_message( - '/products/{}/trades'.format(product_id), - rate_limiter=self.p_rate_limiter + "/products/{}/trades".format(product_id), rate_limiter=self.p_rate_limiter ) - return (self._convert_dict(trade, field_conversions) - for trade in trades) - - def get_product_historic_rates(self, - product_id: str, - start: Optional[str] = None, - stop: Optional[str] = None, - granularity: Optional[str] = None - ) -> List[Dict[str, Any]]: + return (self._convert_dict(trade, field_conversions) for trade in trades) + + def get_product_historic_rates( + self, + product_id: str, + start: Optional[str] = None, + stop: Optional[str] = None, + granularity: Optional[str] = None, + ) -> List[Dict[str, Any]]: """Get historic rates for a product. Rates are returned in grouped buckets based on requested @@ -270,28 +285,31 @@ def get_product_historic_rates(self, See `get_products()`. """ + def convert_candle(c): out = dict() - out['time'] = datetime.utcfromtimestamp(c[0]) - out['low'] = c[1] - out['high'] = c[2] - out['open'] = c[3] - out['close'] = c[4] - out['volume'] = c[5] + out["time"] = datetime.utcfromtimestamp(c[0]) + out["low"] = c[1] + out["high"] = c[2] + out["open"] = c[3] + out["close"] = c[4] + out["volume"] = c[5] return out params = {} if start is not None: - params['start'] = start + params["start"] = start if stop is not None: - params['end'] = stop + params["end"] = stop if granularity is not None: - params['granularity'] = granularity + params["granularity"] = granularity - candles = self._send_message('get', - '/products/{}/candles'.format(product_id), - params=params, - rate_limiter=self.p_rate_limiter) + candles = self._send_message( + "get", + "/products/{}/candles".format(product_id), + params=params, + rate_limiter=self.p_rate_limiter, + ) return [convert_candle(c) for c in candles] def get_product_24hr_stats(self, product_id: str) -> Dict[str, Any]: @@ -316,15 +334,19 @@ def get_product_24hr_stats(self, product_id: str) -> Dict[str, Any]: See `get_products()`. """ - field_conversions = {'open': Decimal, - 'high': Decimal, - 'low': Decimal, - 'volume': Decimal, - 'last': Decimal, - 'volume_30day': Decimal} - stats = self._send_message('get', - '/products/{}/stats'.format(product_id), - rate_limiter=self.p_rate_limiter) + field_conversions = { + "open": Decimal, + "high": Decimal, + "low": Decimal, + "volume": Decimal, + "last": Decimal, + "volume_30day": Decimal, + } + stats = self._send_message( + "get", + "/products/{}/stats".format(product_id), + rate_limiter=self.p_rate_limiter, + ) return self._convert_dict(stats, field_conversions) def get_currencies(self) -> List[Dict[str, Any]]: @@ -366,10 +388,10 @@ def get_currencies(self) -> List[Dict[str, Any]]: See `get_products()`. """ - field_conversions = {'min_size': Decimal} - currencies = self._send_message('get', - '/currencies', - rate_limiter=self.p_rate_limiter) + field_conversions = {"min_size": Decimal} + currencies = self._send_message( + "get", "/currencies", rate_limiter=self.p_rate_limiter + ) return self._convert_list_of_dicts(currencies, field_conversions) def get_time(self) -> Dict[str, Any]: @@ -387,17 +409,15 @@ def get_time(self) -> Dict[str, Any]: See `get_products()`. """ - field_conversions = {'iso': self._parse_datetime} - times = self._send_message('get', - '/time', - rate_limiter=self.p_rate_limiter) + field_conversions = {"iso": self._parse_datetime} + times = self._send_message("get", "/time", rate_limiter=self.p_rate_limiter) return self._convert_dict(times, field_conversions) @staticmethod def _check_errors_and_raise(response): """Check for error codes and raise an exception if necessary.""" if 400 <= response.status_code < 600: - message = response.json()['message'] + message = response.json()["message"] if response.status_code == 400: raise BadRequest(message) elif response.status_code == 401: @@ -409,13 +429,14 @@ def _check_errors_and_raise(response): else: raise CoinbaseAPIError(message) - def _send_message(self, - method: str, - endpoint: str, - params: Optional[Dict] = None, - data: Optional[str] = None, - rate_limiter: Optional[RateLimiter] = None - ) -> Union[List, Dict]: + def _send_message( + self, + method: str, + endpoint: str, + params: Optional[Dict] = None, + data: Optional[str] = None, + rate_limiter: Optional[RateLimiter] = None, + ) -> Union[List, Dict]: """Sends API request. Args: @@ -435,20 +456,23 @@ def _send_message(self, url = self.url + endpoint if rate_limiter: rate_limiter.rate_limit() - r = self.session.request(method, - url, - params=params, - data=data, - auth=self.auth, - timeout=self.request_timeout) + r = self.session.request( + method, + url, + params=params, + data=data, + auth=self.auth, + timeout=self.request_timeout, + ) self._check_errors_and_raise(r) return r.json(parse_float=Decimal) - def _send_paginated_message(self, - endpoint: str, - params: Optional[Dict] = None, - rate_limiter: Optional[RateLimiter] = None - ) -> Iterator[Dict]: + def _send_paginated_message( + self, + endpoint: str, + params: Optional[Dict] = None, + rate_limiter: Optional[RateLimiter] = None, + ) -> Iterator[Dict]: """Sends API message that results in a paginated response. The paginated responses are abstracted away by making API @@ -484,10 +508,9 @@ def _send_paginated_message(self, while True: if rate_limiter: rate_limiter.rate_limit() - r = self.session.get(url, - params=params, - auth=self.auth, - timeout=self.request_timeout) + r = self.session.get( + url, params=params, auth=self.auth, timeout=self.request_timeout + ) self._check_errors_and_raise(r) results = r.json(parse_float=Decimal) for result in results: @@ -496,18 +519,17 @@ def _send_paginated_message(self, # param to get next page. # If this request included `before` don't get any more pages - the # Coinbase pro API doesn't support multiple pages in that case. - if not r.headers.get('cb-after') or \ - params.get('before') is not None: + if not r.headers.get("cb-after") or params.get("before") is not None: break else: - params['after'] = r.headers['cb-after'] + params["after"] = r.headers["cb-after"] @staticmethod def _parse_datetime(dt): try: - return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S.%fZ') + return datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%fZ") except ValueError: - return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') + return datetime.strptime(dt, "%Y-%m-%dT%H:%M:%SZ") @staticmethod def _convert_dict(r, field_conversions): diff --git a/coinbasepro/rate_limiter.py b/coinbasepro/rate_limiter.py index 5568da2..4a50774 100644 --- a/coinbasepro/rate_limiter.py +++ b/coinbasepro/rate_limiter.py @@ -9,11 +9,12 @@ class RateLimiter: Can be configured with a burst size and long term rate limit. """ + def __init__(self, burst_size: int, rate_limit: int): self.lock = threading.Lock() - self.token_bucket = TokenBucket(max_amount=burst_size, - refill_period=1., - refill_amount=rate_limit) + self.token_bucket = TokenBucket( + max_amount=burst_size, refill_period=1.0, refill_amount=rate_limit + ) def rate_limit(self): """Blocks until a token can be obtained from the bucket.""" diff --git a/coinbasepro/token_bucket.py b/coinbasepro/token_bucket.py index 3fd997f..bd561ef 100644 --- a/coinbasepro/token_bucket.py +++ b/coinbasepro/token_bucket.py @@ -10,10 +10,7 @@ class TokenBucket: time/amount. """ - def __init__(self, - max_amount: int, - refill_period: float, - refill_amount: int): + def __init__(self, max_amount: int, refill_period: float, refill_amount: int): """Create a token bucket. Args: @@ -35,8 +32,12 @@ def _refill_count(self): def time_to_next_token(self): """Time remaining until next token is added to the bucket.""" - return (self.last_update + self.refill_period - time.monotonic() - - self._refill_count() * self.refill_period) + return ( + self.last_update + + self.refill_period + - time.monotonic() + - self._refill_count() * self.refill_period + ) def reset(self): """Reset bucket.""" @@ -46,8 +47,7 @@ def reset(self): def get(self): """Get count of bucket.""" return min( - self.max_amount, - self.value + self._refill_count() * self.refill_amount + self.max_amount, self.value + self._refill_count() * self.refill_amount ) def reduce(self, tokens: int): diff --git a/requirements.txt b/requirements.txt index 7b8afa6..570b153 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,5 @@ -certifi==2019.6.16 -chardet==3.0.4 -idna==2.8 -pkginfo==1.4.2 -Pygments==2.2.0 -requests==2.22.0 -requests-toolbelt==0.8.0 -tqdm==4.23.4 -twine==1.11.0 -urllib3==1.25.3 +certifi==2020.12.5 +chardet==4.0.0 +idna==2.10 +requests==2.25.1 +urllib3==1.26.2 diff --git a/setup.py b/setup.py index fa8006e..aa05fcf 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,36 @@ import setuptools -with open('README.rst', 'r') as fh: +with open("README.rst", "r") as fh: readme = fh.read() -with open('HISTORY.rst', 'r') as f: +with open("HISTORY.rst", "r") as f: history = f.read() -requires = [ - 'requests>=2.20.0' -] +requires = ["requests>=2.20.0"] setuptools.setup( - name='coinbasepro', - version='0.2.1', - description='A Python interface for the Coinbase Pro API.', - long_description=readme + '\n\n' + history, - long_description_content_type='text/x-rst', - license='MIT', - author='Alex Contryman', - author_email='acontry@gmail.com', - url='https://github.com/acontry/coinbasepro', + name="coinbasepro", + version="0.2.1", + description="A Python interface for the Coinbase Pro API.", + long_description=readme + "\n\n" + history, + long_description_content_type="text/x-rst", + license="MIT", + author="Alex Contryman", + author_email="acontry@gmail.com", + url="https://github.com/acontry/coinbasepro", packages=setuptools.find_packages(), - python_requires='>=3.4.x', + python_requires=">=3.4.x", install_requires=requires, classifiers=( - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7' + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Natural Language :: English", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ), ) From c0207ffca49a46317ee3328b24b36f1ae1d64ba0 Mon Sep 17 00:00:00 2001 From: Alex Contryman Date: Thu, 7 Jan 2021 19:58:04 -0800 Subject: [PATCH 2/2] Add github action for pre-commit --- .github/workflows/pre-commit.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/pre-commit.yaml diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..7233479 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0