diff --git a/examples/readonly.py b/examples/readonly.py index 9d4f1f8..1978f5d 100644 --- a/examples/readonly.py +++ b/examples/readonly.py @@ -1,3 +1,5 @@ +"""Example script demonstrating read-only API calls to Luno.""" + import os import time diff --git a/luno_python/__init__.py b/luno_python/__init__.py index a1d5a2f..308cd67 100644 --- a/luno_python/__init__.py +++ b/luno_python/__init__.py @@ -1 +1,3 @@ +"""Luno Python SDK.""" + VERSION = "0.0.10" diff --git a/luno_python/base_client.py b/luno_python/base_client.py index fba61d3..8351e53 100644 --- a/luno_python/base_client.py +++ b/luno_python/base_client.py @@ -1,8 +1,9 @@ +"""Base HTTP client for Luno API.""" + import json import platform import requests -import six try: from json.decoder import JSONDecodeError @@ -20,8 +21,11 @@ class BaseClient: + """Base HTTP client for making authenticated requests to the Luno API.""" + def __init__(self, base_url="", timeout=0, api_key_id="", api_key_secret=""): - """ + """Initialise the base client. + :type base_url: str :type timeout: float :type api_key_id: str @@ -34,7 +38,7 @@ def __init__(self, base_url="", timeout=0, api_key_id="", api_key_secret=""): self.session = requests.Session() def set_auth(self, api_key_id, api_key_secret): - """Provides the client with an API key and secret. + """Set the API key and secret for authentication. :type api_key_id: str :type api_key_secret: str @@ -43,7 +47,7 @@ def set_auth(self, api_key_id, api_key_secret): self.api_key_secret = api_key_secret def set_base_url(self, base_url): - """Overrides the default base URL. For internal use. + """Set the base URL for API requests. :type base_url: str """ @@ -52,7 +56,7 @@ def set_base_url(self, base_url): self.base_url = base_url.rstrip("/") def set_timeout(self, timeout): - """Sets the timeout, in seconds, for requests made by the client. + """Set the timeout in seconds for API requests. :type timeout: float """ @@ -61,9 +65,9 @@ def set_timeout(self, timeout): self.timeout = timeout def do(self, method, path, req=None, auth=False): - """Performs an API request and returns the response. + """Perform an API request and return the response. - TODO: Handle 429s + TODO: Handle 429s. :type method: str :type path: str @@ -76,7 +80,8 @@ def do(self, method, path, req=None, auth=False): try: params = json.loads(json.dumps(req)) except TypeError as e: - raise TypeError("luno: request parameters must be JSON-serializable: %s" % str(e)) from e + msg = "luno: request parameters must be JSON-serializable: %s" + raise TypeError(msg % str(e)) from e headers = {"User-Agent": self.make_user_agent()} args = dict(timeout=self.timeout, params=params, headers=headers) if auth: @@ -92,7 +97,8 @@ def do(self, method, path, req=None, auth=False): raise Exception("luno: unknown API error (%s)" % res.status_code) def make_url(self, path, params): - """ + """Construct the full URL for an API request. + :type path: str :rtype: str """ @@ -102,7 +108,8 @@ def make_url(self, path, params): return self.base_url + "/" + path.lstrip("/") def make_user_agent(self): - """ + """Generate the User-Agent string for API requests. + :rtype: str """ return f"LunoPythonSDK/{VERSION} python/{PYTHON_VERSION} {SYSTEM} {ARCH}" diff --git a/luno_python/client.py b/luno_python/client.py index a6a7dde..1b20d9c 100644 --- a/luno_python/client.py +++ b/luno_python/client.py @@ -1,3 +1,5 @@ +"""Luno API client implementation.""" + from .base_client import BaseClient @@ -19,9 +21,9 @@ class Client(BaseClient): """ def cancel_withdrawal(self, id): - """Makes a call to DELETE /api/1/withdrawals/{id}. + """Make a call to DELETE /api/1/withdrawals/{id}. - Cancels a withdrawal request. + Cancel a withdrawal request. This can only be done if the request is still in state PENDING. Permissions required: Perm_W_Withdrawals @@ -35,15 +37,19 @@ def cancel_withdrawal(self, id): return self.do("DELETE", "/api/1/withdrawals/{id}", req=req, auth=True) def create_account(self, currency, name): - """Makes a call to POST /api/1/accounts. + """Make a call to POST /api/1/accounts. - This request creates an Account for the specified currency. Please note that the balances for the Account will be displayed based on the asset value, which is the currency the Account is based on. + This request creates an Account for the specified currency. Please note that the balances for the Account will + be displayed based on the asset value, which is the currency the Account is based on. Permissions required: Perm_W_Addresses - :param currency: The currency code for the Account you want to create. Please see the Currency section for a detailed list of currencies supported by the Luno platform. + :param currency: The currency code for the Account you want to create. Please see the Currency section for a + detailed list of currencies supported by the Luno platform. - Users must be verified to trade currency in order to be able to create an Account. For more information on the verification process, please see How do I verify my identity?. + Users must be verified to trade currency in order to be able to create an Account. For more + information on the verification process, please see + How do I verify my identity?. Users have a limit of 10 accounts per currency. :type currency: str @@ -57,7 +63,7 @@ def create_account(self, currency, name): return self.do("POST", "/api/1/accounts", req=req, auth=True) def create_beneficiary(self, account_type, bank_account_number, bank_name, bank_recipient): - """Makes a call to POST /api/1/beneficiaries. + """Make a call to POST /api/1/beneficiaries. Create a new beneficiary. @@ -81,9 +87,9 @@ def create_beneficiary(self, account_type, bank_account_number, bank_name, bank_ return self.do("POST", "/api/1/beneficiaries", req=req, auth=True) def create_funding_address(self, asset, account_id=None, name=None): - """Makes a call to POST /api/1/funding_address. + """Make a call to POST /api/1/funding_address. - Allocates a new receive address to your account. There is a rate limit of 1 + Allocate a new receive address to your account. There is a rate limit of 1 address per hour, but bursts of up to 10 addresses are allowed. Only 1 Ethereum receive address can be created. @@ -104,9 +110,9 @@ def create_funding_address(self, asset, account_id=None, name=None): return self.do("POST", "/api/1/funding_address", req=req, auth=True) def create_withdrawal(self, amount, type, beneficiary_id=None, external_id=None, fast=None, reference=None): - """Makes a call to POST /api/1/withdrawals. + """Make a call to POST /api/1/withdrawals. - Creates a new withdrawal request to the specified beneficiary. + Create a new withdrawal request to the specified beneficiary. Permissions required: Perm_W_Withdrawals @@ -116,15 +122,17 @@ def create_withdrawal(self, amount, type, beneficiary_id=None, external_id=None, :type type: str :param beneficiary_id: The beneficiary ID of the bank account the withdrawal will be paid out to. This parameter is required if the user has set up multiple beneficiaries. - The beneficiary ID can be found by selecting on the beneficiary name on the user’s Beneficiaries page. + The beneficiary ID can be found by selecting on the beneficiary name on the user's + Beneficiaries page. :type beneficiary_id: int :param external_id: Optional unique ID to associate with this withdrawal. Useful to prevent duplicate sends. This field supports all alphanumeric characters including "-" and "_". :type external_id: str :param fast: If true, it will be a fast withdrawal if possible. Fast withdrawals come with a fee. - Currently fast withdrawals are only available for `type=ZAR_EFT`; for other types, an error is returned. - Fast withdrawals are not possible for Bank of Baroda, Deutsche Bank, Merrill Lynch South Africa, UBS, Postbank and Tyme Bank. + Currently fast withdrawals are only available for `type=ZAR_EFT`; for other types, an error is + returned. Fast withdrawals are not possible for Bank of Baroda, Deutsche Bank, Merrill Lynch South + Africa, UBS, Postbank and Tyme Bank. The fee to be charged is the same as when withdrawing from the UI. :type fast: bool :param reference: For internal use. @@ -142,7 +150,7 @@ def create_withdrawal(self, amount, type, beneficiary_id=None, external_id=None, return self.do("POST", "/api/1/withdrawals", req=req, auth=True) def delete_beneficiary(self, id): - """Makes a call to DELETE /api/1/beneficiaries/{id}. + """Make a call to DELETE /api/1/beneficiaries/{id}. Delete a beneficiary @@ -157,7 +165,7 @@ def delete_beneficiary(self, id): return self.do("DELETE", "/api/1/beneficiaries/{id}", req=req, auth=True) def get_balances(self, assets=None, account_id=None): - """Makes a call to GET /api/1/balance. + """Make a call to GET /api/1/balance. The list of all Accounts and their respective balances for the requesting user. @@ -190,7 +198,7 @@ def get_balances(self, assets=None, account_id=None): return response def get_candles(self, duration, pair, since): - """Makes a call to GET /api/exchange/1/candles. + """Make a call to GET /api/exchange/1/candles. Get candlestick market data from the specified time until now, from the oldest to the most recent. @@ -216,9 +224,10 @@ def get_candles(self, duration, pair, since): return self.do("GET", "/api/exchange/1/candles", req=req, auth=True) def get_fee_info(self, pair): - """Makes a call to GET /api/1/fee_info. + """Make a call to GET /api/1/fee_info. - Returns the fees and 30 day trading volume (as of midnight) for a given currency pair. For complete details, please see Fees & Features. + Return the fees and 30 day trading volume (as of midnight) for a given currency pair. For complete details, + please see Fees & Features. Permissions required: Perm_R_Orders @@ -231,12 +240,13 @@ def get_fee_info(self, pair): return self.do("GET", "/api/1/fee_info", req=req, auth=True) def get_funding_address(self, asset, address=None): - """Makes a call to GET /api/1/funding_address. + """Make a call to GET /api/1/funding_address. - Returns the default receive address associated with your account and the - amount received via the address. Users can specify an optional address parameter to return information for a non-default receive address. - In the response, total_received is the total confirmed amount received excluding unconfirmed transactions. - total_unconfirmed is the total sum of unconfirmed receive transactions. + Return the default receive address associated with your account and the + amount received via the address. Users can specify an optional address parameter to return information for a + non-default receive address. + In the response, total_received is the total confirmed amount received excluding unconfirmed + transactions. total_unconfirmed is the total sum of unconfirmed receive transactions. Permissions required: Perm_R_Addresses @@ -253,7 +263,7 @@ def get_funding_address(self, asset, address=None): return self.do("GET", "/api/1/funding_address", req=req, auth=True) def get_move(self, client_move_id=None, id=None): - """Makes a call to GET /api/exchange/1/move. + """Make a call to GET /api/exchange/1/move. Get a specific move funds instruction by either id or client_move_id. If both are provided an API error will be @@ -261,8 +271,8 @@ def get_move(self, client_move_id=None, id=None): Permissions required: MP_None - :param client_move_id: Get by the user defined ID. This is mutually exclusive with id and is required if id is - not provided. + :param client_move_id: Get by the user defined ID. This is mutually exclusive with id and is + required if id is not provided. :type client_move_id: str :param id: Get by the system ID. This is mutually exclusive with client_move_id and is required if client_move_id is not provided. @@ -275,7 +285,7 @@ def get_move(self, client_move_id=None, id=None): return self.do("GET", "/api/exchange/1/move", req=req, auth=True) def get_order(self, id): - """Makes a call to GET /api/1/orders/{id}. + """Make a call to GET /api/1/orders/{id}. Get an Order's details by its ID. @@ -290,7 +300,7 @@ def get_order(self, id): return self.do("GET", "/api/1/orders/{id}", req=req, auth=True) def get_order_book(self, pair): - """Makes a call to GET /api/1/orderbook_top. + """Make a call to GET /api/1/orderbook_top. This request returns the best 100 `bids` and `asks`, for the currency pair specified, in the Order Book. @@ -307,7 +317,7 @@ def get_order_book(self, pair): return self.do("GET", "/api/1/orderbook_top", req=req, auth=False) def get_order_book_full(self, pair): - """Makes a call to GET /api/1/orderbook. + """Make a call to GET /api/1/orderbook. This request returns all `bids` and `asks`, for the currency pair specified, in the Order Book. @@ -328,7 +338,7 @@ def get_order_book_full(self, pair): return self.do("GET", "/api/1/orderbook", req=req, auth=False) def get_order_v2(self, id): - """Makes a call to GET /api/exchange/2/orders/{id}. + """Make a call to GET /api/exchange/2/orders/{id}. Get the details for an order. @@ -343,7 +353,7 @@ def get_order_v2(self, id): return self.do("GET", "/api/exchange/2/orders/{id}", req=req, auth=True) def get_order_v3(self, client_order_id=None, id=None): - """Makes a call to GET /api/exchange/3/order. + """Make a call to GET /api/exchange/3/order. Get the details for an order by order reference or client order ID. Exactly one of the two parameters must be provided, otherwise an error is returned. @@ -361,9 +371,9 @@ def get_order_v3(self, client_order_id=None, id=None): return self.do("GET", "/api/exchange/3/order", req=req, auth=True) def get_ticker(self, pair): - """Makes a call to GET /api/1/ticker. + """Make a call to GET /api/1/ticker. - Returns the latest ticker indicators for the specified currency pair. + Return the latest ticker indicators for the specified currency pair. Please see the Currency list for the complete list of supported currency pairs. @@ -376,9 +386,9 @@ def get_ticker(self, pair): return self.do("GET", "/api/1/ticker", req=req, auth=False) def get_tickers(self, pair=None): - """Makes a call to GET /api/1/tickers. + """Make a call to GET /api/1/tickers. - Returns the latest ticker indicators from all active Luno exchanges. + Return the latest ticker indicators from all active Luno exchanges. Please see the Currency list for the complete list of supported currency pairs. @@ -393,9 +403,9 @@ def get_tickers(self, pair=None): return self.do("GET", "/api/1/tickers", req=req, auth=False) def get_withdrawal(self, id): - """Makes a call to GET /api/1/withdrawals/{id}. + """Make a call to GET /api/1/withdrawals/{id}. - Returns the status of a particular withdrawal request. + Return the status of a particular withdrawal request. Permissions required: Perm_R_Withdrawals @@ -408,9 +418,9 @@ def get_withdrawal(self, id): return self.do("GET", "/api/1/withdrawals/{id}", req=req, auth=True) def list_beneficiaries(self, bank_recipient=None): - """Makes a call to GET /api/1/beneficiaries. + """Make a call to GET /api/1/beneficiaries. - Returns a list of bank beneficiaries. + Return a list of bank beneficiaries. Permissions required: Perm_R_Beneficiaries @@ -422,9 +432,9 @@ def list_beneficiaries(self, bank_recipient=None): return self.do("GET", "/api/1/beneficiaries", req=req, auth=True) def list_moves(self, before=None, limit=None): - """Makes a call to GET /api/exchange/1/move/list_moves. + """Make a call to GET /api/exchange/1/move/list_moves. - Returns a list of the most recent moves ordered from newest to oldest. + Return a list of the most recent moves ordered from newest to oldest. This endpoint will list up to 100 most recent moves by default. Permissions required: MP_None @@ -441,9 +451,9 @@ def list_moves(self, before=None, limit=None): return self.do("GET", "/api/exchange/1/move/list_moves", req=req, auth=True) def list_orders(self, created_before=None, limit=None, pair=None, state=None): - """Makes a call to GET /api/1/listorders. + """Make a call to GET /api/1/listorders. - Returns a list of the most recently placed Orders. + Return a list of the most recently placed Orders. Users can specify an optional state=PENDING parameter to restrict the results to only open Orders. Users can also specify the market by using the optional currency pair parameter. @@ -467,9 +477,9 @@ def list_orders(self, created_before=None, limit=None, pair=None, state=None): return self.do("GET", "/api/1/listorders", req=req, auth=True) def list_orders_v2(self, closed=None, created_before=None, limit=None, pair=None): - """Makes a call to GET /api/exchange/2/listorders. + """Make a call to GET /api/exchange/2/listorders. - Returns a list of the most recently placed orders ordered from newest to + Return a list of the most recently placed orders ordered from newest to oldest. This endpoint will list up to 100 most recent open orders by default. @@ -495,7 +505,7 @@ def list_orders_v2(self, closed=None, created_before=None, limit=None, pair=None return self.do("GET", "/api/exchange/2/listorders", req=req, auth=True) def list_pending_transactions(self, id): - """Makes a call to GET /api/1/accounts/{id}/pending. + """Make a call to GET /api/1/accounts/{id}/pending. Return a list of all transactions that have not completed for the Account. @@ -512,9 +522,9 @@ def list_pending_transactions(self, id): return self.do("GET", "/api/1/accounts/{id}/pending", req=req, auth=True) def list_trades(self, pair, since=None): - """Makes a call to GET /api/1/trades. + """Make a call to GET /api/1/trades. - Returns a list of recent trades for the specified currency pair. At most + Return a list of recent trades for the specified currency pair. At most 100 trades are returned per call and never trades older than 24h. The trades are sorted from newest to oldest. @@ -535,7 +545,7 @@ def list_trades(self, pair, since=None): return self.do("GET", "/api/1/trades", req=req, auth=False) def list_transactions(self, id, max_row, min_row): - """Makes a call to GET /api/1/accounts/{id}/transactions. + """Make a call to GET /api/1/accounts/{id}/transactions. Return a list of transaction entries from an account. @@ -565,9 +575,9 @@ def list_transactions(self, id, max_row, min_row): return self.do("GET", "/api/1/accounts/{id}/transactions", req=req, auth=True) def list_transfers(self, account_id, before=None, limit=None): - """Makes a call to GET /api/exchange/1/transfers. + """Make a call to GET /api/exchange/1/transfers. - Returns a list of the most recent confirmed transfers ordered from newest to + Return a list of the most recent confirmed transfers ordered from newest to oldest. This includes bank transfers, card payments, or on-chain transactions that have been reflected on your account available balance. @@ -607,15 +617,16 @@ def list_user_trades( since=None, sort_desc=None, ): - """Makes a call to GET /api/1/listtrades. + """Make a call to GET /api/1/listtrades. - Returns a list of the recent Trades for a given currency pair for this user, sorted by oldest first. + Return a list of the recent Trades for a given currency pair for this user, sorted by oldest first. If before is specified, then Trades are returned sorted by most-recent first. type in the response indicates the type of Order that was placed to participate in the trade. Possible types: BID, ASK. - If is_buy in the response is true, then the Order which completed the trade (market taker) was a Bid Order. + If is_buy in the response is true, then the Order which completed the trade (market taker) was a + Bid Order. Results of this query may lag behind the latest data. @@ -651,9 +662,9 @@ def list_user_trades( return self.do("GET", "/api/1/listtrades", req=req, auth=True) def list_withdrawals(self, before_id=None, limit=None): - """Makes a call to GET /api/1/withdrawals. + """Make a call to GET /api/1/withdrawals. - Returns a list of withdrawal requests. + Return a list of withdrawal requests. Permissions required: Perm_R_Withdrawals @@ -670,7 +681,7 @@ def list_withdrawals(self, before_id=None, limit=None): return self.do("GET", "/api/1/withdrawals", req=req, auth=True) def markets(self, pair=None): - """Makes a call to GET /api/exchange/1/markets. + """Make a call to GET /api/exchange/1/markets. List all supported markets parameter information like price scale, min and max order volumes and market ID. @@ -684,7 +695,7 @@ def markets(self, pair=None): return self.do("GET", "/api/exchange/1/markets", req=req, auth=False) def move(self, amount, credit_account_id, debit_account_id, client_move_id=None): - """Makes a call to POST /api/exchange/1/move. + """Make a call to POST /api/exchange/1/move. Move funds between two of your transactional accounts with the same currency The funds may not be moved by the time the request returns. The GET method @@ -701,10 +712,12 @@ def move(self, amount, credit_account_id, debit_account_id, client_move_id=None) :param debit_account_id: The account to debit the funds from. :type debit_account_id: int :param client_move_id: Client move ID. - May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). Maximum length: 255. - It will be available in read endpoints, so you can use it to avoid duplicate moves between the same accounts. - Values must be unique across all your successful calls of this endpoint; trying to create a move request - with the same `client_move_id` as one of your past move requests will result in a HTTP 409 Conflict response. + May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). + Maximum length: 255. It will be available in read endpoints, so you can use it to avoid + duplicate moves between the same accounts. Values must be unique across all your + successful calls of this endpoint; trying to create a move request with the same + `client_move_id` as one of your past move requests will result in a HTTP 409 Conflict + response. :type client_move_id: str """ req = { @@ -731,7 +744,7 @@ def post_limit_order( timestamp=None, ttl=None, ): - """Makes a call to POST /api/1/postorder. + """Make a call to POST /api/1/postorder. Warning! Orders cannot be reversed once they have executed. Please ensure your program has been thoroughly tested before submitting Orders. @@ -754,34 +767,38 @@ def post_limit_order( :param base_account_id: The base currency Account to use in the trade. :type base_account_id: int :param client_order_id: Client order ID. - May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). Maximum length: 255. - It will be available in read endpoints, so you can use it to reconcile Luno with your internal system. - Values must be unique across all your successful order creation endpoint calls; trying to create an order - with the same `client_order_id` as one of your past orders will result in a HTTP 409 Conflict response. + May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). + Maximum length: 255. It will be available in read endpoints, so you can use it to + reconcile Luno with your internal system. Values must be unique across all your + successful order creation endpoint calls; trying to create an order with the same + `client_order_id` as one of your past orders will result in a HTTP 409 Conflict + response. :type client_order_id: str :param counter_account_id: The counter currency Account to use in the trade. :type counter_account_id: int - :param post_only: Post-only Orders will be cancelled if they would otherwise have traded - immediately. + :param post_only: Post-only Orders will be cancelled if they would otherwise have traded immediately. For example, if there's a bid at ZAR 100,000 and you place a post-only ask at ZAR 100,000, your order will be cancelled instead of trading. If the best bid is ZAR 100,000 and you place a post-only ask at ZAR 101,000, your order won't trade but will go into the order book. :type post_only: bool - :param stop_direction: Side of the trigger price to activate the order. This should be set if `stop_price` is also - set. + :param stop_direction: Side of the trigger price to activate the order. This should be set if `stop_price` is + also set. - `RELATIVE_LAST_TRADE` will automatically infer the direction based on the last - trade price and the stop price. If last trade price is less than stop price then stop - direction is ABOVE otherwise is BELOW. + `RELATIVE_LAST_TRADE` will automatically infer the direction based on the last trade + price and the stop price. If last trade price is less than stop price then stop direction + is ABOVE otherwise is BELOW. :type stop_direction: str :param stop_price: Trigger trade price to activate this order as a decimal string. If this is set then this is treated as a Stop Limit Order and `stop_direction` is expected to be set too. :type stop_price: float - :param time_in_force: GTC Good 'Til Cancelled. The order remains open until it is filled or cancelled by the user.
- IOC Immediate Or Cancel. The part of the order that cannot be filled immediately will be cancelled. Cannot be post-only.
- FOK Fill Or Kill. If the order cannot be filled immediately and completely it will be cancelled before any trade. Cannot be post-only. + :param time_in_force: GTC Good 'Til Cancelled. The order remains open until it is filled or + cancelled by the user.
+ IOC Immediate Or Cancel. The part of the order that cannot be filled + immediately will be cancelled. Cannot be post-only.
+ FOK Fill Or Kill. If the order cannot be filled immediately and completely it + will be cancelled before any trade. Cannot be post-only. :type time_in_force: str :param timestamp: Unix timestamp in milliseconds of when the request was created and sent. :type timestamp: int @@ -818,15 +835,17 @@ def post_market_order( timestamp=None, ttl=None, ): - """Makes a call to POST /api/1/marketorder. + """Make a call to POST /api/1/marketorder. - A Market Order executes immediately, and either buys as much of the asset that can be bought for a set amount of fiat currency, or sells a set amount of the asset for as much as possible. + A Market Order executes immediately, and either buys as much of the asset that can be bought for a set amount of + fiat currency, or sells a set amount of the asset for as much as possible. Warning! Orders cannot be reversed once they have executed. Please ensure your program has been thoroughly tested before submitting Orders. - If no base_account_id or counter_account_id are specified, the default base currency or counter currency account will be used. - Users can find their account IDs by calling the Balances request. + If no base_account_id or counter_account_id are specified, the default base + currency or counter currency account will be used. Users can find their account IDs by calling the + Balances request. Permissions required: Perm_W_Orders @@ -837,17 +856,21 @@ def post_market_order( :type type: str :param base_account_id: The base currency account to use in the trade. :type base_account_id: int - :param base_volume: For a SELL order: amount of the base currency to use (e.g. how much BTC to sell for EUR in the BTC/EUR market) + :param base_volume: For a SELL order: amount of the base currency to use (e.g. how much BTC to sell + for EUR in the BTC/EUR market) :type base_volume: float :param client_order_id: Client order ID. - May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). Maximum length: 255. - It will be available in read endpoints, so you can use it to reconcile Luno with your internal system. - Values must be unique across all your successful order creation endpoint calls; trying to create an order - with the same `client_order_id` as one of your past orders will result in a HTTP 409 Conflict response. + May only contain alphanumeric (0-9, a-z, or A-Z) and special characters (_ ; , . -). + Maximum length: 255. It will be available in read endpoints, so you can use it to + reconcile Luno with your internal system. Values must be unique across all your + successful order creation endpoint calls; trying to create an order with the same + `client_order_id` as one of your past orders will result in a HTTP 409 Conflict + response. :type client_order_id: str :param counter_account_id: The counter currency account to use in the trade. :type counter_account_id: int - :param counter_volume: For a BUY order: amount of the counter currency to use (e.g. how much EUR to use to buy BTC in the BTC/EUR market) + :param counter_volume: For a BUY order: amount of the counter currency to use (e.g. how much EUR to + use to buy BTC in the BTC/EUR market) :type counter_volume: float :param timestamp: Unix timestamp in milliseconds of when the request was created and sent. :type timestamp: int @@ -884,9 +907,10 @@ def send( memo=None, message=None, ): - """Makes a call to POST /api/1/send. + """Make a call to POST /api/1/send. - Send assets from an Account. Please note that the asset type sent must match the receive address of the same cryptocurrency of the same type - Bitcoin to Bitcoin, Ethereum to Ethereum, etc. + Send assets from an Account. Please note that the asset type sent must match the receive address of the same + cryptocurrency of the same type - Bitcoin to Bitcoin, Ethereum to Ethereum, etc. Sends can be made to cryptocurrency receive addresses. @@ -899,7 +923,8 @@ def send( Note: :type address: str @@ -907,26 +932,34 @@ def send( :type amount: float :param currency: Currency to send. :type currency: str - :param account_id: Optional source account. In case of multiple accounts for a single currency, the source account that will provide the funds for the transaction may be specified. If omitted, the default account will be used. + :param account_id: Optional source account. In case of multiple accounts for a single currency, the source + account that will provide the funds for the transaction may be specified. If omitted, the + default account will be used. :type account_id: int :param description: User description for the transaction to record on the account statement. :type description: str - :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is provided. + :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is + provided. :type destination_tag: int :param external_id: Optional unique ID to associate with this withdrawal. Useful to prevent duplicate sends in case of failure. This supports all alphanumeric characters, as well as "-" and "_". :type external_id: str - :param forex_notice_self_declaration: Only required for Foreign Exchange Notification under the Malaysia FEN rules. ForexNoticeSelfDeclaration must be true if the user has exceeded his/her annual investment limit in foreign currency assets. + :param forex_notice_self_declaration: Only required for Foreign Exchange Notification under the Malaysia FEN + rules. ForexNoticeSelfDeclaration must be true if the user has exceeded + his/her annual investment limit in foreign currency assets. :type forex_notice_self_declaration: bool - :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if zero). + :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if + zero). :type has_destination_tag: bool - :param is_drb: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsDRB must be true if the user has Domestic Ringgit Borrowing (DRB). + :param is_drb: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsDRB must be true + if the user has Domestic Ringgit Borrowing (DRB). :type is_drb: bool - :param is_forex_send: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsForexSend must be true if sending to an address hosted outside of Malaysia. + :param is_forex_send: Only required for Foreign Exchange Notification under the Malaysia FEN rules. IsForexSend + must be true if sending to an address hosted outside of Malaysia. :type is_forex_send: bool - :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" information - for a generic address. + :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" + information for a generic address. :type memo: str :param message: Message to send to the recipient. This is only relevant when sending to an email address. @@ -950,7 +983,7 @@ def send( return self.do("POST", "/api/1/send", req=req, auth=True) def send_fee(self, address, amount, currency): - """Makes a call to GET /api/1/send_fee. + """Make a call to GET /api/1/send_fee. Calculate fees involved with a crypto send request. @@ -963,7 +996,8 @@ def send_fee(self, address, amount, currency): Note: :type address: str @@ -980,7 +1014,7 @@ def send_fee(self, address, amount, currency): return self.do("GET", "/api/1/send_fee", req=req, auth=True) def stop_order(self, order_id): - """Makes a call to POST /api/1/stoporder. + """Make a call to POST /api/1/stoporder. Request to cancel an Order. @@ -998,7 +1032,7 @@ def stop_order(self, order_id): return self.do("POST", "/api/1/stoporder", req=req, auth=True) def update_account_name(self, id, name): - """Makes a call to PUT /api/1/accounts/{id}/name. + """Make a call to PUT /api/1/accounts/{id}/name. Update the name of an account with a given ID. @@ -1034,7 +1068,7 @@ def validate( physical_address=None, wallet_name=None, ): - """Makes a call to POST /api/1/address/validate. + """Make a call to POST /api/1/address/validate. Validate receive addresses, to which a customer wishes to make cryptocurrency sends, are verified under covering regulatory requirements for the customer such as travel rules. @@ -1046,7 +1080,8 @@ def validate( Note: :type address: str @@ -1058,32 +1093,40 @@ def validate( :type beneficiary_name: str :param country: Country is the ISO 3166-1 country code of the beneficial owner of the address :type country: str - :param date_of_birth: DateOfBirth is the date of birth of the (non-institutional) beneficial owner of the address in the form "YYYY-MM-DD" + :param date_of_birth: DateOfBirth is the date of birth of the (non-institutional) beneficial owner of the + address in the form "YYYY-MM-DD" :type date_of_birth: str - :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is provided. + :param destination_tag: Optional XRP destination tag. Note that HasDestinationTag must be true if this value is + provided. :type destination_tag: int - :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if zero). + :param has_destination_tag: Optional boolean flag indicating that a XRP destination tag is provided (even if + zero). :type has_destination_tag: bool - :param institution_name: InstitutionName is the name of the beneficial owner if is it is a legal entities address + :param institution_name: InstitutionName is the name of the beneficial owner if is it is a legal entities + address :type institution_name: str - :param is_legal_entity: IsLegalEntity indicates if the address is for a legal entity and not a private beneficiary. - If this field is true then the fields BeneficiaryName, Nationality & DateOfBirth should be empty but the - fields InstitutionName and Country should be populated. - If this field is false and IsSelfSend is false (or empty) then the field InstitutionName should be empty but the - fields BeneficiaryName, Nationality & DateOfBirth and Country should be populated. + :param is_legal_entity: IsLegalEntity indicates if the address is for a legal entity and not a private + beneficiary. If this field is true then the fields BeneficiaryName, Nationality & + DateOfBirth should be empty but the fields InstitutionName and Country should be + populated. If this field is false and IsSelfSend is false (or empty) then the field + InstitutionName should be empty but the fields BeneficiaryName, Nationality & + DateOfBirth and Country should be populated. :type is_legal_entity: bool - :param is_private_wallet: IsPrivateWallet indicates if the address is for private wallet and not held at an exchange. + :param is_private_wallet: IsPrivateWallet indicates if the address is for private wallet and not held at an + exchange. :type is_private_wallet: bool :param is_self_send: IsSelfSend to indicate that the address belongs to the customer. If this field is true then the remaining omitempty fields should not be populated. :type is_self_send: bool - :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" information - for a generic address. + :param memo: Optional memo string used to provide account information for ATOM, etc. where it holds "account" + information for a generic address. :type memo: str - :param nationality: Nationality ISO 3166-1 country code of the nationality of the (non-institutional) beneficial owner of the address + :param nationality: Nationality ISO 3166-1 country code of the nationality of the (non-institutional) beneficial + owner of the address :type nationality: str - :param physical_address: PhysicalAddress is the legal physical address of the beneficial owner of the crypto address + :param physical_address: PhysicalAddress is the legal physical address of the beneficial owner of the crypto + address :type physical_address: str :param wallet_name: PrivateWalletName is the name of the private wallet :type wallet_name: str diff --git a/luno_python/error.py b/luno_python/error.py index ce28465..6658542 100644 --- a/luno_python/error.py +++ b/luno_python/error.py @@ -1,4 +1,16 @@ +"""Luno API error classes.""" + + class APIError(Exception): + """Exception raised for Luno API errors.""" + def __init__(self, code, message): + """Initialise APIError with code and message. + + :param code: Error code from the API + :type code: str + :param message: Error message from the API + :type message: str + """ self.code = code self.message = message diff --git a/pyproject.toml b/pyproject.toml index e4369ed..5423a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,10 @@ skip_gitignore = true [tool.bandit] exclude_dirs = ["tests", "env", "build"] -skips = ["B101"] # Skip assert_used check (common in tests) +skips = [ + "B101", # Skip assert_used check (common in tests) + "B107", # Skip hardcoded_password_default (empty string defaults are acceptable for optional credentials) +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/setup.py b/setup.py index 3c8a0b3..cdf0021 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +"""Setup script for luno-python package.""" + from setuptools import find_packages, setup from luno_python import VERSION diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..a70437e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for luno-python.""" diff --git a/tests/test_client.py b/tests/test_client.py index 2ee8885..7d17d41 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,7 +1,8 @@ +"""Tests for the Luno Python client.""" + from decimal import Decimal import pytest -import requests import requests_mock try: @@ -83,6 +84,7 @@ def test_client(): + """Test client initialization and configuration.""" c = Client() c.set_auth("api_key_id", "api_key_secret") c.set_base_url("base_url") @@ -95,6 +97,7 @@ def test_client(): def test_client_do_basic(): + """Test basic client do method functionality.""" c = Client() c.set_base_url("mock://test/") @@ -102,7 +105,7 @@ def test_client_do_basic(): c.session.mount("mock", adapter) adapter.register_uri("GET", "mock://test/", text="ok") - with pytest.raises(Exception): + with pytest.raises(Exception, match="unknown API error"): res = c.do("GET", "/") adapter.register_uri("GET", "mock://test/", text='{"key":"value"}') @@ -120,7 +123,7 @@ def test_client_do_basic(): def test_get_balances_without_account_id(): - """Test get_balances without account_id parameter (backward compatibility)""" + """Test get_balances without account_id parameter (backward compatibility).""" c = Client() c.set_base_url("mock://test/") @@ -137,7 +140,7 @@ def test_get_balances_without_account_id(): def test_get_balances_with_valid_account_id(): - """Test get_balances with valid account_id parameter""" + """Test get_balances with valid account_id parameter.""" c = Client() c.set_base_url("mock://test/") @@ -170,7 +173,7 @@ def test_get_balances_with_valid_account_id(): def test_get_balances_with_invalid_account_id(): - """Test get_balances with invalid account_id parameter""" + """Test get_balances with invalid account_id parameter.""" c = Client() c.set_base_url("mock://test/") @@ -185,7 +188,7 @@ def test_get_balances_with_invalid_account_id(): def test_get_balances_with_account_id_and_assets(): - """Test get_balances with both account_id and assets parameters""" + """Test get_balances with both account_id and assets parameters.""" c = Client() c.set_base_url("mock://test/") @@ -207,7 +210,7 @@ def test_get_balances_with_account_id_and_assets(): def test_get_balances_with_account_id_type_conversion(): - """Test get_balances with account_id type conversion (string vs int)""" + """Test get_balances with account_id type conversion (string vs int).""" c = Client() c.set_base_url("mock://test/") @@ -229,7 +232,7 @@ def test_get_balances_with_account_id_type_conversion(): def test_get_balances_with_empty_balance_response(): - """Test get_balances when API returns empty balance list""" + """Test get_balances when API returns empty balance list.""" c = Client() c.set_base_url("mock://test/") @@ -248,7 +251,7 @@ def test_get_balances_with_empty_balance_response(): def test_get_balances_with_malformed_response(): - """Test get_balances when API returns malformed response""" + """Test get_balances when API returns malformed response.""" c = Client() c.set_base_url("mock://test/")