├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── src └── buycoins │ ├── __init__.py │ ├── client.py │ ├── exceptions.py │ └── modules │ ├── __init__.py │ ├── accounts.py │ ├── orders.py │ ├── p2p.py │ ├── transactions.py │ └── webhook.py └── tests ├── __init__.py ├── conftest.py ├── test_accounts.py ├── test_orders.py ├── test_p2p.py ├── test_transactions.py ├── test_webhook.py └── utils.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | 6 | before_install: 7 | - pip install poetry 8 | 9 | install: 10 | - poetry install 11 | 12 | script: 13 | - poetry run black . 14 | - poetry run pytest -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome all kinds of contributions: 4 | 5 | - Bug fixes 6 | - Documentation improvements 7 | - Refactoring & tidying 8 | - Feature improvements 9 | 10 | See something that can be improved? Please make a pull request! 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Buycoins Python 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buycoins Python Library 2 | 3 | [![Build Status](https://travis-ci.com/edgeee/buycoins-python.svg?token=oQSNV8eQ1aycrRUjPbyg&branch=main)](https://travis-ci.com/edgeee/buycoins-python) [![Circle CI](https://img.shields.io/badge/license-MIT-blue.svg)](https://img.shields.io/badge/license-MIT-blue.svg) [![PyPI version](https://badge.fury.io/py/buycoins.svg)](https://badge.fury.io/py/buycoins) [![Python 3.6+](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) 4 | 5 | This library provides easy access to the Buycoins API using the Python programming language. It provides all the feature of the API so that you don't need to interact with the API directly. This library can be used with Python 3.6+ 6 | 7 | ## Table of Contents 8 | * [Links](#links) 9 | * [Installation](#installation) 10 | * [Introduction](#introduction) 11 | * [Primer](#primer) 12 | * [Initialization](#initialization) 13 | * [Accounts](#accounts) 14 | * [Create Naira deposit account](#create-naira-deposit-account) 15 | * [Orders](#orders) 16 | * [Get cryptocurrency prices](#get-cryptocurrency-prices) 17 | * [Get price for single cryptocurrency](#get-single-cryptocurrency-price) 18 | * [Buy a cryptocurrency](#buy-a-cryptocurrency) 19 | * [Sell a cryptocurrency](#sell-a-cryptocurrency) 20 | * [P2P Trading](#p2p-trading) 21 | * [Place limit orders](#place-limit-orders) 22 | * [Place market order](#place-market-orders) 23 | * [Get list of orders](#get-list-of-orders) 24 | * [Get market book](#get-market-book) 25 | * [Transactions](#transactions) 26 | * [Get cryptocurrency balances](#get-balances) 27 | * [Get single balance](#get-single-balance) 28 | * [Estimate network fee](#estimate-network-fee) 29 | * [Create wallet address](#create-wallet-address) 30 | * [Send cryptocurrency](#send-cryptocurrency-to-an-address) 31 | * [Webhooks](#webhooks) 32 | * [Verify event payload](#verify-event-payload) 33 | * [Contributing](#contributing) 34 | * [License](#license) 35 | 36 | 37 | 38 | ## Links 39 | - Buycoins API documentation: https://developers.buycoins.africa/ 40 | 41 | ## Installation 42 | You can install this package using pip: 43 | ```sh 44 | pip install --upgrade buycoins 45 | ``` 46 | 47 | ## Introduction 48 | 49 | #### Primer 50 | - The library is structured around the concept of a `type`, so everything is a type. 51 | - All date quantities are specified as timestamps. So you would have to reconstruct the ISO dates yourself if you ever need to. 52 | - All cryptocurrency (and monetary) values are specified as decimals. 53 | - Supports all cryptocurrencies supported by Buycoins 54 | 55 | #### Initialization 56 | Firstly, request API access by sending an email to [support@buycoins.africa](mailto:support@buycoins.africa) with the email address you used in creating a Buycoins account. 57 | When you've been granted access, you should be able to generate a public and secret key from the 'API settings' section of your account. 58 | 59 | You have to initialize the library once in your app. You can do this when initializing databases, logging, etc. 60 | As a security practice, it is best not to hardcode your API keys. You should store them in environmental variables or a remote Secrets Manager. 61 | 62 | ```python 63 | import buycoins 64 | 65 | buycoins.initialize('', '') 66 | ``` 67 | 68 | ### Accounts 69 | Accounts provide a way to programmatically create virtual naira accounts. 70 | 71 | #### Types 72 | ```dtd 73 | VirtualDepositAccountType: 74 | account_number: str 75 | account_name: str 76 | account_type: str 77 | bank_name: str 78 | account_reference: str 79 | ``` 80 | 81 | #### Create Naira deposit account 82 | ```python 83 | import buycoins as bc 84 | 85 | acc = bc.accounts.create_deposit('john doe') # VirtualDepositAccountType 86 | 87 | print(acc.account_name) # john doe 88 | print(acc.bank_name) # bank name 89 | print(acc.account_number) # account number 90 | print(acc.account_reference) # account reference 91 | print(acc.account_type) # account type 92 | ``` 93 | 94 | ### Orders 95 | Orders provide a way to buy from and sell directly to Buycoins. 96 | When buying or selling, price ID should be the ID gotten from calling either `.get_price()` or `.get_prices()`. 97 | Make sure to use price that hasn't expired yet, so call `.get_price(cryptocurrency)` to get the latest price for the cryptocurrency just before buying or selling. 98 | 99 | 100 | #### Types 101 | ```dtd 102 | CoinPriceType: 103 | id: str 104 | cryptocurrency: str 105 | buy_price_per_coin: Decimal 106 | min_buy: Decimal 107 | max_buy: Decimal 108 | expires_at: int 109 | 110 | class OrderType(NamedTuple): 111 | id: str 112 | cryptocurrency: str 113 | status: str 114 | side: str 115 | created_at: int 116 | total_coin_amount: str 117 | static_price: Decimal 118 | price_type: str 119 | dynamic_exchange_rate: str 120 | coin_amount: Decimal 121 | ``` 122 | 123 | #### Get cryptocurrency prices 124 | ```python 125 | import buycoins as bc 126 | 127 | # Get prices of all cryptocurrencies 128 | prices = bc.orders.get_prices() # CoinPriceType[] 129 | 130 | print(prices[0].id) # ID of first price entry 131 | print(prices[0].cryptocurrency) # cryptocurrency 132 | print(prices[0].expires_at) # when this price entry will expire 133 | print(prices[0].buy_price_per_coin) # coin price 134 | print(prices[0].min_buy) # minimum amount you can buy 135 | print(prices[0].max_buy) # max amount you can buy 136 | ``` 137 | 138 | #### Get single cryptocurrency price 139 | ```python 140 | import buycoins as bc 141 | 142 | price = bc.orders.get_price('bitcoin') # CoinPriceType 143 | 144 | print(price.id) # ID of price entry 145 | print(price.cryptocurrency) # cryptocurrency 146 | print(price.expires_at) # when this price entry will expire 147 | print(price.buy_price_per_coin) # coin price 148 | print(price.min_buy) # minimum amount you can buy 149 | print(price.max_buy) # max amount you can buy 150 | ``` 151 | 152 | #### Buy a cryptocurrency 153 | ```python 154 | import buycoins as bc 155 | 156 | 157 | order = bc.orders.buy( 158 | price_id='price-id', 159 | coin_amount=1.52, 160 | cryptocurrency='litecoin' 161 | ) # OrderType 162 | 163 | print(order.id) # Order ID 164 | print(order.status) # either active or inactive 165 | print(order.side) # buy 166 | print(order.cryptocurrency) # litecoin 167 | print(order.total_coin_amount) # Total coin amount 168 | print(order.price_type) # Price type 169 | ``` 170 | 171 | #### Sell a cryptocurrency 172 | ```python 173 | import buycoins as bc 174 | 175 | 176 | order = bc.orders.sell( 177 | price_id='price-id', 178 | coin_amount=0.0043, 179 | cryptocurrency='ethereum' 180 | ) # OrderType 181 | 182 | print(order.id) # Order ID 183 | print(order.status) # either active or inactive 184 | print(order.side) # sell 185 | print(order.cryptocurrency) # litecoin 186 | print(order.total_coin_amount) # Total coin amount 187 | print(order.price_type) # Price type 188 | ``` 189 | 190 | 191 | ### P2P Trading 192 | P2P Trading lets you trade cryptocurrencies with other users. 193 | If you are not familiar with p2p trading on the Buycoins platform, read about it [here](https://developers.buycoins.africa/p2p/introduction) 194 | 195 | #### Types 196 | ```dtd 197 | class OrderType(NamedTuple): 198 | id: str 199 | cryptocurrency: str 200 | status: str 201 | side: str 202 | created_at: int 203 | total_coin_amount: str 204 | static_price: Decimal 205 | price_type: str 206 | dynamic_exchange_rate: str 207 | coin_amount: Decimal 208 | ``` 209 | 210 | ### Place limit orders 211 | When placing limit orders, if `price_type` is `static`, `static_price` must also be specified, and if `price_type` is `dynamic`, `dynamic_exchange_rate` must be provided. 212 | 213 | ```python 214 | import buycoins as bc 215 | 216 | 217 | order = bc.p2p.place_limit_order( 218 | side='buy', # either 'buy' or 'sell' 219 | coin_amount=0.00043, 220 | cryptocurrency='ethereum', 221 | price_type='static', 222 | static_price=0.004, 223 | dynamic_exchange_rate=None # float 224 | ) # OrderType 225 | 226 | print(order.id) # Order ID 227 | print(order.status) # status, either active or inactive 228 | print(order.cryptocurrency) # bitcoin, litecoin, etc 229 | print(order.coin_amount) # coin amount 230 | ``` 231 | 232 | #### Place market orders 233 | ```python 234 | import buycoins as bc 235 | 236 | 237 | # Place market order 238 | # `order` has all the properties as shown above 239 | order = bc.p2p.place_market_order( 240 | side='buy', # either buy or sell 241 | coin_amount=0.00023, 242 | cryptocurrency='litecoin' 243 | ) # order is an OrderType 244 | 245 | print(order.id) # Order ID 246 | print(order.status) # status, either active or inactive 247 | print(order.cryptocurrency) # bitcoin, litecoin, etc 248 | print(order.coin_amount) # coin amount 249 | ``` 250 | 251 | #### Get list of orders 252 | ```python 253 | import buycoins as bc 254 | 255 | 256 | orders, dynamic_price_expiry = bc.p2p.get_orders('active') # (OrderType[], timestamp) 257 | 258 | print(dynamic_price_expiry) # timestamp of when dynamic price expires 259 | print(orders[0].id) # ID of first order 260 | print(orders[1].status) # status of the first order 261 | ``` 262 | 263 | 264 | #### Get Market book 265 | ```python 266 | import buycoins as bc 267 | 268 | 269 | market_book, dynamic_price_expiry = bc.p2p.get_market_book() # (OrderType[], timestamp) 270 | 271 | print(dynamic_price_expiry) # timestamp of when dynamic price expires 272 | print(market_book[0].id) # ID of first order 273 | print(market_book[1].status) # status of the first order 274 | ``` 275 | 276 | 277 | ### Transactions 278 | 279 | Transactions enable you to send and receive cryptocurrencies. 280 | 281 | #### Types 282 | ```dtd 283 | CoinBalanceType: 284 | id: str 285 | cryptocurrency: str 286 | confirmed_balance: Decimal 287 | 288 | NetworkFeeType: 289 | estimated_fee: Decimal 290 | total: Decimal 291 | 292 | TransactionType: 293 | hash: str 294 | id: str 295 | 296 | SendReturnValueType: 297 | id: str 298 | address: str 299 | cryptocurrency: str 300 | amount: Decimal 301 | fee: Decimal 302 | status: str 303 | transaction: TransactionType 304 | 305 | AddressType: 306 | cryptocurrency: str 307 | address: str 308 | ``` 309 | 310 | #### Get balances 311 | ```python 312 | import buycoins as bc 313 | 314 | balances = bc.transactions.get_balances() # CoinBalanceType[] 315 | 316 | print(balances[0].cryptocurrency) # bitcoin, litecoin, etc 317 | print(balances[0].confirmed_balance) # the confirmed balance 318 | ``` 319 | 320 | 321 | #### Get single balance 322 | ```python 323 | import buycoins as bc 324 | 325 | 326 | balance = bc.transactions.get_balance('bitcoin') # CoinBalanceType 327 | 328 | print(balance.cryptocurrency) # bitcoin 329 | print(balance.confirmed_balance) # the confirmed balance 330 | ``` 331 | 332 | #### Estimate network fee 333 | ```python 334 | import buycoins as bc 335 | 336 | 337 | fee = bc.transactions.estimate_network_fee( 338 | 'bitcoin', # cryptocurrency 339 | 0.32, # txn amount 340 | ) # NetworkFeeType 341 | 342 | print(fee.estimated_fee) # estimated fee for txn 343 | print(fee.total) # total 344 | ``` 345 | 346 | #### Send cryptocurrency to an address 347 | ```python 348 | import buycoins as bc 349 | 350 | 351 | sent = bc.transactions.send( 352 | cryptocurrency='ethereum', 353 | amount=0.0023, 354 | address='' 355 | ) # SendReturnValueType 356 | 357 | print(sent.fee) # fee charged for the 'send' txn 358 | print(sent.status) # status of the txn 359 | print(sent.transaction.id) # ID of the txn 360 | print(sent.transaction.hash) # txn hash 361 | ``` 362 | 363 | #### Create wallet address 364 | ```python 365 | import buycoins as bc 366 | 367 | 368 | addr = bc.transactions.create_address('bitcoin') # AddressType 369 | 370 | print(addr.address) # Address string 371 | print(addr.cryptocurrency) # cryptocurrency 372 | ``` 373 | 374 | 375 | ### Webhooks 376 | 377 | Webhooks provides a way for Buycoins to inform you of events that take place on your account. 378 | See the [Buycoins documentation](https://developers.buycoins.africa/webhooks/introduction) for an introduction and the available events. 379 | 380 | #### Verify event payload 381 | Ensure that the webhook event originated from Buycoins 382 | 383 | ```python 384 | import buycoins as bc 385 | 386 | 387 | is_valid = bc.webhook.verify_payload( 388 | body='', 389 | webhook_token='', 390 | header_signature='X-Webhook-Signature header' 391 | ) 392 | 393 | print(is_valid) # True if the event is from Buycoins, False otherwise. 394 | ``` 395 | 396 | ## Testing 397 | To run tests: 398 | 399 | ```shell 400 | poetry run pytest 401 | ``` 402 | 403 | 404 | ## Contributing 405 | See [CONTRIBUTING.md](CONTRIBUTING.md) 406 | 407 | 408 | ## License 409 | [MIT License](https://github.com/edgeee/buycoins-python/blob/master/LICENSE) 410 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "atomicwrites" 11 | version = "1.4.0" 12 | description = "Atomic file writes." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "20.3.0" 20 | description = "Classes Without Boilerplate" 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | 25 | [package.extras] 26 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 27 | docs = ["furo", "sphinx", "zope.interface"] 28 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 29 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 30 | 31 | [[package]] 32 | name = "black" 33 | version = "20.8b1" 34 | description = "The uncompromising code formatter." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=3.6" 38 | 39 | [package.dependencies] 40 | appdirs = "*" 41 | click = ">=7.1.2" 42 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 43 | mypy-extensions = ">=0.4.3" 44 | pathspec = ">=0.6,<1" 45 | regex = ">=2020.1.8" 46 | toml = ">=0.10.1" 47 | typed-ast = ">=1.4.0" 48 | typing-extensions = ">=3.7.4" 49 | 50 | [package.extras] 51 | colorama = ["colorama (>=0.4.3)"] 52 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 53 | 54 | [[package]] 55 | name = "certifi" 56 | version = "2020.12.5" 57 | description = "Python package for providing Mozilla's CA Bundle." 58 | category = "main" 59 | optional = false 60 | python-versions = "*" 61 | 62 | [[package]] 63 | name = "chardet" 64 | version = "4.0.0" 65 | description = "Universal encoding detector for Python 2 and 3" 66 | category = "main" 67 | optional = false 68 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 69 | 70 | [[package]] 71 | name = "click" 72 | version = "7.1.2" 73 | description = "Composable command line interface toolkit" 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 77 | 78 | [[package]] 79 | name = "colorama" 80 | version = "0.4.4" 81 | description = "Cross-platform colored terminal text." 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | 86 | [[package]] 87 | name = "dataclasses" 88 | version = "0.8" 89 | description = "A backport of the dataclasses module for Python 3.6" 90 | category = "dev" 91 | optional = false 92 | python-versions = ">=3.6, <3.7" 93 | 94 | [[package]] 95 | name = "distlib" 96 | version = "0.3.1" 97 | description = "Distribution utilities" 98 | category = "dev" 99 | optional = false 100 | python-versions = "*" 101 | 102 | [[package]] 103 | name = "filelock" 104 | version = "3.0.12" 105 | description = "A platform independent file lock." 106 | category = "dev" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [[package]] 111 | name = "gql" 112 | version = "2.0.0" 113 | description = "GraphQL client for Python" 114 | category = "main" 115 | optional = false 116 | python-versions = "*" 117 | 118 | [package.dependencies] 119 | graphql-core = ">=2.3.2,<3" 120 | promise = ">=2.3,<3" 121 | requests = ">=2.12,<3" 122 | six = ">=1.10.0" 123 | 124 | [package.extras] 125 | dev = ["flake8 (==3.8.1)", "isort (==4.3.21)", "black (==19.10b0)", "mypy (==0.770)", "check-manifest (>=0.42,<1)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "coveralls (==2.0.0)"] 126 | test = ["pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "coveralls (==2.0.0)"] 127 | 128 | [[package]] 129 | name = "graphql-core" 130 | version = "2.3.2" 131 | description = "GraphQL implementation for Python" 132 | category = "main" 133 | optional = false 134 | python-versions = "*" 135 | 136 | [package.dependencies] 137 | promise = ">=2.3,<3" 138 | rx = ">=1.6,<2" 139 | six = ">=1.10.0" 140 | 141 | [package.extras] 142 | gevent = ["gevent (>=1.1)"] 143 | test = ["six (==1.14.0)", "pyannotate (==1.2.0)", "pytest (==4.6.10)", "pytest-django (==3.9.0)", "pytest-cov (==2.8.1)", "coveralls (==1.11.1)", "cython (==0.29.17)", "gevent (==1.5.0)", "pytest-benchmark (==3.2.3)", "pytest-mock (==2.0.0)"] 144 | 145 | [[package]] 146 | name = "httpretty" 147 | version = "1.0.5" 148 | description = "HTTP client mock for Python" 149 | category = "dev" 150 | optional = false 151 | python-versions = ">=3" 152 | 153 | [[package]] 154 | name = "idna" 155 | version = "2.10" 156 | description = "Internationalized Domain Names in Applications (IDNA)" 157 | category = "main" 158 | optional = false 159 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 160 | 161 | [[package]] 162 | name = "importlib-metadata" 163 | version = "3.4.0" 164 | description = "Read metadata from Python packages" 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.6" 168 | 169 | [package.dependencies] 170 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 171 | zipp = ">=0.5" 172 | 173 | [package.extras] 174 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 175 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 176 | 177 | [[package]] 178 | name = "importlib-resources" 179 | version = "5.1.0" 180 | description = "Read resources from Python packages" 181 | category = "dev" 182 | optional = false 183 | python-versions = ">=3.6" 184 | 185 | [package.dependencies] 186 | zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} 187 | 188 | [package.extras] 189 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 190 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"] 191 | 192 | [[package]] 193 | name = "more-itertools" 194 | version = "8.7.0" 195 | description = "More routines for operating on iterables, beyond itertools" 196 | category = "dev" 197 | optional = false 198 | python-versions = ">=3.5" 199 | 200 | [[package]] 201 | name = "mypy-extensions" 202 | version = "0.4.3" 203 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 204 | category = "dev" 205 | optional = false 206 | python-versions = "*" 207 | 208 | [[package]] 209 | name = "packaging" 210 | version = "20.9" 211 | description = "Core utilities for Python packages" 212 | category = "dev" 213 | optional = false 214 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 215 | 216 | [package.dependencies] 217 | pyparsing = ">=2.0.2" 218 | 219 | [[package]] 220 | name = "pathspec" 221 | version = "0.8.1" 222 | description = "Utility library for gitignore style pattern matching of file paths." 223 | category = "dev" 224 | optional = false 225 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 226 | 227 | [[package]] 228 | name = "pluggy" 229 | version = "0.13.1" 230 | description = "plugin and hook calling mechanisms for python" 231 | category = "dev" 232 | optional = false 233 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 234 | 235 | [package.dependencies] 236 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 237 | 238 | [package.extras] 239 | dev = ["pre-commit", "tox"] 240 | 241 | [[package]] 242 | name = "promise" 243 | version = "2.3" 244 | description = "Promises/A+ implementation for Python" 245 | category = "main" 246 | optional = false 247 | python-versions = "*" 248 | 249 | [package.dependencies] 250 | six = "*" 251 | 252 | [package.extras] 253 | test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"] 254 | 255 | [[package]] 256 | name = "py" 257 | version = "1.10.0" 258 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 262 | 263 | [[package]] 264 | name = "pyparsing" 265 | version = "2.4.7" 266 | description = "Python parsing module" 267 | category = "dev" 268 | optional = false 269 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 270 | 271 | [[package]] 272 | name = "pytest" 273 | version = "5.4.3" 274 | description = "pytest: simple powerful testing with Python" 275 | category = "dev" 276 | optional = false 277 | python-versions = ">=3.5" 278 | 279 | [package.dependencies] 280 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 281 | attrs = ">=17.4.0" 282 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 283 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 284 | more-itertools = ">=4.0.0" 285 | packaging = "*" 286 | pluggy = ">=0.12,<1.0" 287 | py = ">=1.5.0" 288 | wcwidth = "*" 289 | 290 | [package.extras] 291 | checkqa-mypy = ["mypy (==v0.761)"] 292 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 293 | 294 | [[package]] 295 | name = "regex" 296 | version = "2020.11.13" 297 | description = "Alternative regular expression module, to replace re." 298 | category = "dev" 299 | optional = false 300 | python-versions = "*" 301 | 302 | [[package]] 303 | name = "requests" 304 | version = "2.25.1" 305 | description = "Python HTTP for Humans." 306 | category = "main" 307 | optional = false 308 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 309 | 310 | [package.dependencies] 311 | certifi = ">=2017.4.17" 312 | chardet = ">=3.0.2,<5" 313 | idna = ">=2.5,<3" 314 | urllib3 = ">=1.21.1,<1.27" 315 | 316 | [package.extras] 317 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 318 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 319 | 320 | [[package]] 321 | name = "rx" 322 | version = "1.6.1" 323 | description = "Reactive Extensions (Rx) for Python" 324 | category = "main" 325 | optional = false 326 | python-versions = "*" 327 | 328 | [[package]] 329 | name = "six" 330 | version = "1.15.0" 331 | description = "Python 2 and 3 compatibility utilities" 332 | category = "main" 333 | optional = false 334 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 335 | 336 | [[package]] 337 | name = "toml" 338 | version = "0.10.2" 339 | description = "Python Library for Tom's Obvious, Minimal Language" 340 | category = "dev" 341 | optional = false 342 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 343 | 344 | [[package]] 345 | name = "tox" 346 | version = "3.22.0" 347 | description = "tox is a generic virtualenv management and test command line tool" 348 | category = "dev" 349 | optional = false 350 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 351 | 352 | [package.dependencies] 353 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 354 | filelock = ">=3.0.0" 355 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 356 | packaging = ">=14" 357 | pluggy = ">=0.12.0" 358 | py = ">=1.4.17" 359 | six = ">=1.14.0" 360 | toml = ">=0.9.4" 361 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 362 | 363 | [package.extras] 364 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 365 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] 366 | 367 | [[package]] 368 | name = "typed-ast" 369 | version = "1.4.2" 370 | description = "a fork of Python 2 and 3 ast modules with type comment support" 371 | category = "dev" 372 | optional = false 373 | python-versions = "*" 374 | 375 | [[package]] 376 | name = "typing-extensions" 377 | version = "3.7.4.3" 378 | description = "Backported and Experimental Type Hints for Python 3.5+" 379 | category = "dev" 380 | optional = false 381 | python-versions = "*" 382 | 383 | [[package]] 384 | name = "urllib3" 385 | version = "1.26.4" 386 | description = "HTTP library with thread-safe connection pooling, file post, and more." 387 | category = "main" 388 | optional = false 389 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 390 | 391 | [package.extras] 392 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 393 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 394 | brotli = ["brotlipy (>=0.6.0)"] 395 | 396 | [[package]] 397 | name = "virtualenv" 398 | version = "20.4.2" 399 | description = "Virtual Python Environment builder" 400 | category = "dev" 401 | optional = false 402 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 403 | 404 | [package.dependencies] 405 | appdirs = ">=1.4.3,<2" 406 | distlib = ">=0.3.1,<1" 407 | filelock = ">=3.0.0,<4" 408 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 409 | importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} 410 | six = ">=1.9.0,<2" 411 | 412 | [package.extras] 413 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 414 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] 415 | 416 | [[package]] 417 | name = "wcwidth" 418 | version = "0.2.5" 419 | description = "Measures the displayed width of unicode strings in a terminal" 420 | category = "dev" 421 | optional = false 422 | python-versions = "*" 423 | 424 | [[package]] 425 | name = "zipp" 426 | version = "3.4.0" 427 | description = "Backport of pathlib-compatible object wrapper for zip files" 428 | category = "dev" 429 | optional = false 430 | python-versions = ">=3.6" 431 | 432 | [package.extras] 433 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 434 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 435 | 436 | [metadata] 437 | lock-version = "1.1" 438 | python-versions = "~3.6" 439 | content-hash = "24fd7f4c64bdb19bbc22ce2297340140d34ef0f42f9d1e824b00054aee557b8e" 440 | 441 | [metadata.files] 442 | appdirs = [ 443 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 444 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 445 | ] 446 | atomicwrites = [ 447 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 448 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 449 | ] 450 | attrs = [ 451 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 452 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 453 | ] 454 | black = [ 455 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 456 | ] 457 | certifi = [ 458 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 459 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 460 | ] 461 | chardet = [ 462 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 463 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 464 | ] 465 | click = [ 466 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 467 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 468 | ] 469 | colorama = [ 470 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 471 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 472 | ] 473 | dataclasses = [ 474 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 475 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 476 | ] 477 | distlib = [ 478 | {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, 479 | {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, 480 | ] 481 | filelock = [ 482 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 483 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 484 | ] 485 | gql = [ 486 | {file = "gql-2.0.0-py2.py3-none-any.whl", hash = "sha256:35032ddd4bfe6b8f3169f806b022168932385d751eacc5c5f7122e0b3f4d6b88"}, 487 | {file = "gql-2.0.0.tar.gz", hash = "sha256:fe8d3a08047f77362ddfcfddba7cae377da2dd66f5e61c59820419c9283d4fb5"}, 488 | ] 489 | graphql-core = [ 490 | {file = "graphql-core-2.3.2.tar.gz", hash = "sha256:aac46a9ac524c9855910c14c48fc5d60474def7f99fd10245e76608eba7af746"}, 491 | {file = "graphql_core-2.3.2-py2.py3-none-any.whl", hash = "sha256:44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad"}, 492 | ] 493 | httpretty = [ 494 | {file = "httpretty-1.0.5.tar.gz", hash = "sha256:e53c927c4d3d781a0761727f1edfad64abef94e828718e12b672a678a8b3e0b5"}, 495 | ] 496 | idna = [ 497 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 498 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 499 | ] 500 | importlib-metadata = [ 501 | {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, 502 | {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, 503 | ] 504 | importlib-resources = [ 505 | {file = "importlib_resources-5.1.0-py3-none-any.whl", hash = "sha256:885b8eae589179f661c909d699a546cf10d83692553e34dca1bf5eb06f7f6217"}, 506 | {file = "importlib_resources-5.1.0.tar.gz", hash = "sha256:bfdad047bce441405a49cf8eb48ddce5e56c696e185f59147a8b79e75e9e6380"}, 507 | ] 508 | more-itertools = [ 509 | {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, 510 | {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, 511 | ] 512 | mypy-extensions = [ 513 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 514 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 515 | ] 516 | packaging = [ 517 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 518 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 519 | ] 520 | pathspec = [ 521 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 522 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 523 | ] 524 | pluggy = [ 525 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 526 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 527 | ] 528 | promise = [ 529 | {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, 530 | ] 531 | py = [ 532 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 533 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 534 | ] 535 | pyparsing = [ 536 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 537 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 538 | ] 539 | pytest = [ 540 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 541 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 542 | ] 543 | regex = [ 544 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 545 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 546 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 547 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 548 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 549 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 550 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 551 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 552 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 553 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 554 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 555 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 556 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 557 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 558 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 559 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 560 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 561 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 562 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 563 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 564 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 565 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 566 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 567 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 568 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 569 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 570 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 571 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 572 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 573 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 574 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 575 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 576 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 577 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 578 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 579 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 580 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 581 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 582 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 583 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 584 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 585 | ] 586 | requests = [ 587 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 588 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 589 | ] 590 | rx = [ 591 | {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"}, 592 | {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"}, 593 | ] 594 | six = [ 595 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 596 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 597 | ] 598 | toml = [ 599 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 600 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 601 | ] 602 | tox = [ 603 | {file = "tox-3.22.0-py2.py3-none-any.whl", hash = "sha256:89afa9c59c04beb55eda789c7a65feb1a70fde117f85f1bd1c27c66758456e60"}, 604 | {file = "tox-3.22.0.tar.gz", hash = "sha256:ed1e650cf6368bcbc4a071eeeba363c480920e0ed8a9ad1793c7caaa5ad33d49"}, 605 | ] 606 | typed-ast = [ 607 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 608 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 609 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 610 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 611 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 612 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 613 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 614 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 615 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 616 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 617 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 618 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 619 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 620 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 621 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 622 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 623 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 624 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 625 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 626 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 627 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 628 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 629 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 630 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 631 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 632 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 633 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 634 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 635 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 636 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 637 | ] 638 | typing-extensions = [ 639 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 640 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 641 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 642 | ] 643 | urllib3 = [ 644 | {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, 645 | {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, 646 | ] 647 | virtualenv = [ 648 | {file = "virtualenv-20.4.2-py2.py3-none-any.whl", hash = "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3"}, 649 | {file = "virtualenv-20.4.2.tar.gz", hash = "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d"}, 650 | ] 651 | wcwidth = [ 652 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 653 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 654 | ] 655 | zipp = [ 656 | {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, 657 | {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, 658 | ] 659 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "buycoins" 3 | version = "1.0.2" 4 | description = "A Python SDK for the buycoins API" 5 | authors = ["Rasheed Musa "] 6 | readme = "README.md" 7 | repository = "https://github.com/edgeee/buycoins-python" 8 | documentation = "https://github.com/edgeee/buycoins-python" 9 | 10 | [tool.poetry.dependencies] 11 | python = "~3.6" 12 | gql = "^2.0.0" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "^5.2" 16 | black = "^20.8b1" 17 | tox = "^3.21.3" 18 | httpretty = "^1.0.5" 19 | 20 | [build-system] 21 | requires = ["poetry-core>=1.0.0"] 22 | build-backend = "poetry.core.masonry.api" 23 | 24 | [tool.black] 25 | line-length = 88 26 | target-version = ['py36'] -------------------------------------------------------------------------------- /src/buycoins/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | 3 | from .client import initialize 4 | from .modules import * 5 | -------------------------------------------------------------------------------- /src/buycoins/client.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from typing import Optional, Dict 3 | 4 | from gql import Client, gql 5 | from gql.transport import Transport 6 | from gql.transport.requests import RequestsHTTPTransport 7 | 8 | from buycoins.exceptions import NoConnectionError, ExecutionError, RemoteServerError 9 | 10 | 11 | BUYCOINS_GRAPHQL_ENDPOINT = "https://backend.buycoins.tech/api/graphql" 12 | 13 | 14 | _transport: Optional[Transport] = None 15 | _client: Optional[Client] = None 16 | 17 | 18 | def _make_auth_header(public_key, secret_key): 19 | return "Basic " + b64encode(bytes(f"{public_key}:{secret_key}", "utf8")).decode() 20 | 21 | 22 | def initialize(public_key: str, secret_key: str): 23 | global _transport, _client 24 | if not _client: 25 | headers = {"Authorization": _make_auth_header(public_key, secret_key)} 26 | _transport = RequestsHTTPTransport( 27 | url=BUYCOINS_GRAPHQL_ENDPOINT, headers=headers 28 | ) 29 | _client = Client(transport=_transport, fetch_schema_from_transport=False) 30 | 31 | 32 | def get_client() -> Client: 33 | if not _client: 34 | raise NoConnectionError 35 | return _client 36 | 37 | 38 | def execute_query(document: str, variables: Optional[Dict] = None) -> Dict: 39 | kwargs = {} 40 | if variables is not None: 41 | kwargs["variable_values"] = variables 42 | try: 43 | return get_client().execute(gql(document), **kwargs) 44 | except Exception as exc: 45 | if hasattr(exc, "errors") and len(exc.errors) > 0: # noqa 46 | raise ExecutionError(exc.errors[0]["message"]) 47 | else: 48 | raise RemoteServerError(exc) 49 | -------------------------------------------------------------------------------- /src/buycoins/exceptions.py: -------------------------------------------------------------------------------- 1 | class _BaseException(Exception): 2 | pass 3 | 4 | 5 | class NoConnectionError(_BaseException): 6 | def __init__(self): 7 | super().__init__("No active connection: have you initialized yet?") 8 | 9 | 10 | class ExecutionError(_BaseException): 11 | pass 12 | 13 | 14 | class RemoteServerError(_BaseException): 15 | pass 16 | -------------------------------------------------------------------------------- /src/buycoins/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from . import accounts 2 | from . import p2p 3 | from . import orders 4 | from . import transactions 5 | from . import webhook 6 | -------------------------------------------------------------------------------- /src/buycoins/modules/accounts.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | from buycoins.client import execute_query 4 | 5 | 6 | class VirtualDepositAccountType(NamedTuple): 7 | account_number: str 8 | account_name: str 9 | account_type: str 10 | bank_name: str 11 | account_reference: str 12 | 13 | 14 | def create_deposit(account_name: str) -> VirtualDepositAccountType: 15 | query_str = """ 16 | mutation($name: String!) { 17 | createDepositAccount(accountName: $name) { 18 | accountNumber 19 | accountName 20 | accountType 21 | bankName 22 | accountReference 23 | } 24 | } 25 | """ 26 | 27 | result = execute_query(query_str, dict(name=account_name)) 28 | account = result["createDepositAccount"] 29 | return VirtualDepositAccountType( 30 | account["accountNumber"], 31 | account["accountName"], 32 | account["accountType"], 33 | account["bankName"], 34 | account["accountReference"], 35 | ) 36 | -------------------------------------------------------------------------------- /src/buycoins/modules/orders.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import NamedTuple, List, Dict, Optional 3 | 4 | from buycoins.client import execute_query 5 | 6 | 7 | class CoinPriceType(NamedTuple): 8 | id: str 9 | cryptocurrency: str 10 | buy_price_per_coin: Decimal 11 | min_buy: Decimal 12 | max_buy: Decimal 13 | expires_at: int 14 | 15 | 16 | class OrderType(NamedTuple): 17 | id: str 18 | cryptocurrency: str 19 | status: str 20 | side: str 21 | created_at: int 22 | static_price: Decimal = None 23 | price_type: str = None 24 | dynamic_exchange_rate: str = None 25 | coin_amount: Decimal = None 26 | 27 | 28 | def _make_order(node: Dict) -> OrderType: 29 | return OrderType( 30 | id=node.get("id"), 31 | cryptocurrency=node.get("cryptocurrency"), 32 | coin_amount=node.get("coinAmount", node.get("totalCoinAmount")), 33 | side=node.get("side"), 34 | status=node.get("status"), 35 | created_at=node.get("createdAt"), 36 | price_type=node.get("priceType"), 37 | static_price=node.get("staticPrice", 0), 38 | dynamic_exchange_rate=node.get("dynamicExchangeRate"), 39 | ) 40 | 41 | 42 | def get_prices() -> List[CoinPriceType]: 43 | query_str = """ 44 | query { 45 | getPrices { 46 | id 47 | cryptocurrency 48 | buyPricePerCoin 49 | minBuy 50 | maxBuy 51 | expiresAt 52 | } 53 | } 54 | """ 55 | result = execute_query(query_str) 56 | prices = [] 57 | for price in result["getPrices"]: 58 | record = CoinPriceType._make( 59 | [ 60 | price["id"], 61 | price["cryptocurrency"], 62 | Decimal(price["buyPricePerCoin"]), 63 | Decimal(price["minBuy"]), 64 | Decimal(price["maxBuy"]), 65 | price["expiresAt"], 66 | ] 67 | ) 68 | prices.append(record) 69 | return prices 70 | 71 | 72 | def get_price(cryptocurrency) -> Optional[CoinPriceType]: 73 | for price in get_prices(): 74 | if price.cryptocurrency == cryptocurrency: 75 | return price 76 | return None 77 | 78 | 79 | def _do_order(order_type, *, price_id: str, coin_amount: float, cryptocurrency: str): 80 | query_str = ( 81 | """ 82 | mutation($price: ID!, $amount: BigDecimal!, $crypto: Cryptocurrency) { 83 | %s(price: $price, coin_amount: $amount, cryptocurrency: $crypto) { 84 | id 85 | cryptocurrency 86 | status 87 | totalCoinAmount 88 | side 89 | createdAt 90 | } 91 | } 92 | """ 93 | % order_type 94 | ) 95 | 96 | variables = dict(price=price_id, amount=coin_amount, crypto=cryptocurrency) 97 | result = execute_query(query_str, variables) 98 | return _make_order(result[order_type]) 99 | 100 | 101 | def buy(*, price_id: str, coin_amount: float, cryptocurrency: str): 102 | return _do_order( 103 | "buy", price_id=price_id, coin_amount=coin_amount, cryptocurrency=cryptocurrency 104 | ) 105 | 106 | 107 | def sell(*, price_id: str, coin_amount: float, cryptocurrency: str) -> OrderType: 108 | return _do_order( 109 | "sell", 110 | price_id=price_id, 111 | coin_amount=coin_amount, 112 | cryptocurrency=cryptocurrency, 113 | ) 114 | -------------------------------------------------------------------------------- /src/buycoins/modules/p2p.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any 2 | from buycoins.client import execute_query 3 | from .orders import OrderType, _make_order 4 | 5 | 6 | def place_limit_order( 7 | *, 8 | side: str, 9 | coin_amount: float, 10 | cryptocurrency: str, 11 | price_type: str, 12 | static_price: float = None, 13 | dynamic_exchange_rate: float = None, 14 | ): 15 | query_str = """ 16 | mutation($side: OrderSide!, $amount: BigDecimal!, $crypto: Cryptocurrency, $type_: PriceType!, 17 | $static_price: BigDecimal!, $dynamic_exchange_rate: BigDecimal) { 18 | postLimitOrder(orderSide: $side, coinAmount: $amount, cryptocurrency: $crypto, %s, priceType: $type_){ 19 | id 20 | cryptocurrency 21 | coinAmount 22 | side 23 | status 24 | createdAt 25 | pricePerCoin 26 | priceType 27 | staticPrice 28 | dynamicExchangeRate 29 | } 30 | } 31 | """ % ( 32 | ( 33 | "staticPrice: $static_price" 34 | if price_type == "static" 35 | else "dynamicExchangeRate: $dynamic_exchange_rate" 36 | ) 37 | ) 38 | 39 | variables = dict( 40 | side=side, 41 | amount=coin_amount, 42 | crypto=cryptocurrency, 43 | type_=price_type, 44 | static_price=static_price, 45 | dynamic_exchange_rate=dynamic_exchange_rate, 46 | ) 47 | result = execute_query(query_str, variables) 48 | return _make_order(result["postLimitOrder"]) 49 | 50 | 51 | def place_market_order(*, side: str, coin_amount: float, cryptocurrency: str): 52 | query_str = """ 53 | mutation($side: OrderSide!, $amount: BigDecimal!, $crypto: Cryptocurrency) { 54 | postMarketOrder(orderSide: $side, coinAmount: $amount, cryptocurrency: $crypto){ 55 | id 56 | cryptocurrency 57 | coinAmount 58 | side 59 | status 60 | createdAt 61 | pricePerCoin 62 | priceType 63 | staticPrice 64 | dynamicExchangeRate 65 | } 66 | } 67 | """ 68 | variables = dict(side=side, amount=coin_amount, crypto=cryptocurrency) 69 | result = execute_query(query_str, variables) 70 | return _make_order(result["postMarketOrder"]) 71 | 72 | 73 | def get_orders(status: str) -> Tuple[List[OrderType], Any]: 74 | query_str = """ 75 | query($status: GetOrdersStatus!) { 76 | getOrders(status: $status) { 77 | dynamicPriceExpiry 78 | orders { 79 | edges { 80 | node { 81 | id 82 | cryptocurrency 83 | coinAmount 84 | side 85 | status 86 | createdAt 87 | pricePerCoin 88 | priceType 89 | staticPrice 90 | dynamicExchangeRate 91 | } 92 | } 93 | } 94 | } 95 | } 96 | """ 97 | variables = dict(status=status) 98 | result = execute_query(query_str, variables) 99 | _res = result["getOrders"] 100 | 101 | orders = [] 102 | for o in _res["orders"]["edges"]: 103 | orders.append(_make_order(o["node"])) 104 | return orders, _res["dynamicPriceExpiry"] 105 | 106 | 107 | def get_market_book(): 108 | query_str = """ 109 | query { 110 | getMarketBook { 111 | dynamicPriceExpiry 112 | orders { 113 | edges { 114 | node { 115 | id 116 | cryptocurrency 117 | coinAmount 118 | side 119 | status 120 | createdAt 121 | pricePerCoin 122 | priceType 123 | staticPrice 124 | dynamicExchangeRate 125 | } 126 | } 127 | } 128 | } 129 | } 130 | """ 131 | result = execute_query(query_str) 132 | _res = result["getMarketBook"] 133 | 134 | orders = [] 135 | for o in _res["orders"]["edges"]: 136 | orders.append(_make_order(o["node"])) 137 | return orders, _res["dynamicPriceExpiry"] 138 | -------------------------------------------------------------------------------- /src/buycoins/modules/transactions.py: -------------------------------------------------------------------------------- 1 | from buycoins.client import execute_query 2 | from typing import NamedTuple, List, Optional 3 | from decimal import Decimal 4 | 5 | 6 | class CoinBalanceType(NamedTuple): 7 | id: str 8 | cryptocurrency: str 9 | confirmed_balance: Decimal 10 | 11 | 12 | class NetworkFeeType(NamedTuple): 13 | estimated_fee: Decimal 14 | total: Decimal 15 | 16 | 17 | class TransactionType(NamedTuple): 18 | hash: str 19 | id: str 20 | 21 | 22 | class SendReturnValueType(NamedTuple): 23 | id: str 24 | address: str 25 | cryptocurrency: str 26 | amount: Decimal 27 | fee: Decimal 28 | status: str 29 | transaction: TransactionType 30 | 31 | 32 | class AddressType(NamedTuple): 33 | cryptocurrency: str 34 | address: str 35 | 36 | 37 | def get_balances() -> List[CoinBalanceType]: 38 | query_str = """ 39 | query { 40 | getBalances{ 41 | id 42 | cryptocurrency 43 | confirmedBalance 44 | } 45 | } 46 | """ 47 | result = execute_query(query_str) 48 | balances = [] 49 | for bal in result["getBalances"]: 50 | balances.append( 51 | CoinBalanceType( 52 | id=bal["id"], 53 | cryptocurrency=bal["cryptocurrency"], 54 | confirmed_balance=Decimal(bal["confirmedBalance"]), 55 | ) 56 | ) 57 | return balances 58 | 59 | 60 | def get_balance(cryptocurrency: str) -> Optional[CoinBalanceType]: 61 | for balance in get_balances(): 62 | if balance.cryptocurrency == cryptocurrency: 63 | return balance 64 | return None 65 | 66 | 67 | def estimate_network_fee(cryptocurrency: str, amount: float) -> NetworkFeeType: 68 | query_str = """ 69 | query($crypto: Cryptocurrency, $amount: BigDecimal!) { 70 | getEstimatedNetworkFee(cryptocurrency: $crypto, amount: $amount) { 71 | estimatedFee 72 | total 73 | } 74 | } 75 | """ 76 | variables = dict(crypto=cryptocurrency, amount=amount) 77 | res = execute_query(query_str, variables) 78 | fee = res["getEstimatedNetworkFee"] 79 | 80 | return NetworkFeeType( 81 | estimated_fee=Decimal(fee["estimatedFee"]), total=Decimal(fee["total"]) 82 | ) 83 | 84 | 85 | def send(*, cryptocurrency: str, amount: float, address: str) -> SendReturnValueType: 86 | query_str = """ 87 | mutation($crypto: String!, $amount: Number!, $address: String!) { 88 | send(cryptocurrency: $crypto, amount: $amount, address: $address) { 89 | id 90 | address 91 | amount 92 | cryptocurrency 93 | fee 94 | status 95 | transaction { 96 | txhash 97 | id 98 | } 99 | } 100 | } 101 | """ 102 | variables = dict(crypto=cryptocurrency, amount=amount, address=address) 103 | res = execute_query(query_str, variables) 104 | send_val = res["send"] 105 | return SendReturnValueType( 106 | id=send_val["id"], 107 | address=send_val["address"], 108 | amount=Decimal(send_val["amount"]), 109 | cryptocurrency=send_val["cryptocurrency"], 110 | fee=Decimal(send_val["fee"]), 111 | status=send_val["status"], 112 | transaction=TransactionType( 113 | id=send_val["transaction"]["id"], 114 | hash=send_val["transaction"]["txhash"], 115 | ), 116 | ) 117 | 118 | 119 | def create_address(cryptocurrency: str): 120 | query_str = """ 121 | mutation($crypto: Cryptocurrency) { 122 | createAddress(cryptocurrency: $crypto) { 123 | cryptocurrency 124 | address 125 | } 126 | } 127 | """ 128 | variables = dict(crypto=cryptocurrency) 129 | res = execute_query(query_str, variables) 130 | res = res["createAddress"] 131 | return AddressType(cryptocurrency=res["cryptocurrency"], address=res["address"]) 132 | -------------------------------------------------------------------------------- /src/buycoins/modules/webhook.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | 4 | 5 | def verify_payload(body: bytes, webhook_token: str, header_signature: str) -> bool: 6 | signing_key = webhook_token.encode("utf-8") 7 | if not isinstance(body, bytes): 8 | body = bytes(body) 9 | 10 | hashed = hmac.new(signing_key, body, hashlib.sha1) 11 | return hashed.hexdigest() == header_signature 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgeee/buycoins-python/72a3130cf43d0c618e58418b3d8cb7ce73b0f133/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import httpretty 3 | 4 | import buycoins 5 | 6 | 7 | @pytest.fixture(scope="session", autouse=True) 8 | def execute_before_any_test(): 9 | httpretty.enable() 10 | buycoins.initialize("fake-public-key", "fake-secret-key") 11 | -------------------------------------------------------------------------------- /tests/test_accounts.py: -------------------------------------------------------------------------------- 1 | from tests.utils import _mock_gql 2 | 3 | 4 | create_deposit_response = dict( 5 | createDepositAccount=dict( 6 | accountNumber="123", 7 | accountName="john doe", 8 | accountType="deposit", 9 | bankName="Providus", 10 | accountReference="ref", 11 | ) 12 | ) 13 | 14 | 15 | def test_create_deposit(): 16 | from buycoins import accounts 17 | 18 | _mock_gql(create_deposit_response) 19 | 20 | acc = accounts.create_deposit("john doe") 21 | assert type(acc) == accounts.VirtualDepositAccountType 22 | assert acc.account_number == "123" 23 | assert acc.account_reference == "ref" 24 | assert acc.account_name == "john doe" 25 | -------------------------------------------------------------------------------- /tests/test_orders.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from tests.utils import _mock_gql 4 | 5 | 6 | get_prices_response = dict( 7 | getPrices=[ 8 | dict( 9 | id="1", 10 | cryptocurrency="bitcoin", 11 | buyPricePerCoin="5332823", 12 | minBuy=0.0001, 13 | maxBuy=0.04, 14 | expiresAt=2382084323, 15 | ), 16 | dict( 17 | id="2", 18 | cryptocurrency="litecoin", 19 | buyPricePerCoin="1993", 20 | minBuy=0.001, 21 | maxBuy=5.003, 22 | expiresAt=2382084323, 23 | ), 24 | dict( 25 | id="3", 26 | cryptocurrency="ethereum", 27 | buyPricePerCoin="24384", 28 | minBuy=0.02041, 29 | maxBuy=2, 30 | expiresAt=2382084323, 31 | ), 32 | ] 33 | ) 34 | 35 | sell_coin_response = dict( 36 | sell=dict( 37 | id="1", 38 | cryptocurrency="ethereum", 39 | status="active", 40 | totalCoinAmount=0.0510, 41 | side="sell", 42 | createdAt=230238423, 43 | ) 44 | ) 45 | 46 | buy_response = dict( 47 | buy=dict( 48 | id="10", 49 | cryptocurrency="bitcoin", 50 | status="active", 51 | totalCoinAmount=0.3, 52 | side="buy", 53 | createdAt=230238423, 54 | ) 55 | ) 56 | 57 | 58 | def test_get_prices(): 59 | from buycoins import orders 60 | 61 | _mock_gql(get_prices_response) 62 | 63 | prices = orders.get_prices() 64 | assert 3 == len(prices) 65 | assert all(orders.CoinPriceType == type(price) for price in prices) 66 | assert prices[0].cryptocurrency == "bitcoin" 67 | assert prices[0].id == "1" 68 | 69 | 70 | def test_get_price(): 71 | from buycoins import orders 72 | 73 | _mock_gql(get_prices_response) 74 | 75 | price = orders.get_price("litecoin") 76 | assert orders.CoinPriceType == type(price) 77 | assert price.cryptocurrency == "litecoin" 78 | assert price.id == "2" 79 | assert price.buy_price_per_coin == Decimal(1993) 80 | 81 | 82 | def test_sell(): 83 | from buycoins import orders 84 | 85 | _mock_gql(sell_coin_response) 86 | 87 | resp = orders.sell(price_id="1", coin_amount=0.0510, cryptocurrency="ethereum") 88 | 89 | assert orders.OrderType == type(resp) 90 | assert resp.coin_amount == Decimal(0.0510) 91 | assert resp.status == "active" 92 | assert resp.cryptocurrency == "ethereum" 93 | 94 | 95 | def test_buy(): 96 | from buycoins import orders 97 | 98 | _mock_gql(buy_response) 99 | 100 | buy_order = orders.buy(price_id="10", coin_amount=0.3, cryptocurrency="bitcoin") 101 | 102 | assert orders.OrderType == type(buy_order) 103 | assert buy_order.status == "active" 104 | assert buy_order.coin_amount == Decimal(0.3) 105 | assert buy_order.cryptocurrency == "bitcoin" 106 | -------------------------------------------------------------------------------- /tests/test_p2p.py: -------------------------------------------------------------------------------- 1 | from tests.utils import _mock_gql 2 | 3 | 4 | place_limit_order_response = dict( 5 | postLimitOrder=dict( 6 | id="3", 7 | cryptocurrency="bitcoin", 8 | coinAmount="0.0023", 9 | side="buy", 10 | status="active", 11 | createdAt="2342343", 12 | pricePerCoin="29048", 13 | priceType="static", 14 | staticPrice="2342.23", 15 | dynamicExchangeRate=None, 16 | ) 17 | ) 18 | 19 | place_market_order_response = dict( 20 | postMarketOrder=dict( 21 | id="20", 22 | cryptocurrency="ethereum", 23 | coinAmount="0.0123", 24 | side="sell", 25 | status="active", 26 | createdAt="2342343", 27 | pricePerCoin="29048", 28 | priceType=None, 29 | staticPrice=None, 30 | dynamicExchangeRate=None, 31 | ) 32 | ) 33 | 34 | 35 | get_orders_response = dict( 36 | getOrders=dict( 37 | dynamicPriceExpiry="2340782074", 38 | orders=dict( 39 | edges=[ 40 | dict( 41 | node=dict( 42 | id="4", 43 | cryptocurrency="bitcoin", 44 | coinAmount="0.2023", 45 | status="active", 46 | ) 47 | ), 48 | dict( 49 | node=dict( 50 | id="2", 51 | cryptocurrency="ethereum", 52 | coinAmount="3.0", 53 | status="active", 54 | ) 55 | ), 56 | dict( 57 | node=dict( 58 | id="2", 59 | cryptocurrency="litecoin", 60 | coinAmount="7.340", 61 | status="active", 62 | ) 63 | ), 64 | ] 65 | ), 66 | ) 67 | ) 68 | 69 | get_market_book_response = dict( 70 | getMarketBook=dict( 71 | dynamicPriceExpiry="2340782074", 72 | orders=dict( 73 | edges=[ 74 | dict( 75 | node=dict( 76 | id="2", 77 | cryptocurrency="ethereum", 78 | coinAmount="3.0", 79 | status="active", 80 | ) 81 | ), 82 | dict( 83 | node=dict( 84 | id="2", 85 | cryptocurrency="litecoin", 86 | coinAmount="7.340", 87 | status="active", 88 | ) 89 | ), 90 | ] 91 | ), 92 | ) 93 | ) 94 | 95 | 96 | def test_place_limit_order(): 97 | from buycoins import p2p, orders 98 | 99 | _mock_gql(place_limit_order_response) 100 | 101 | order = p2p.place_limit_order( 102 | side="buy", 103 | coin_amount=0.0023, 104 | cryptocurrency="bitcoin", 105 | price_type="static", 106 | static_price=2342.23, 107 | ) 108 | assert orders.OrderType == type(order) 109 | assert order.coin_amount == "0.0023" 110 | assert order.cryptocurrency == "bitcoin" 111 | assert order.price_type == "static" 112 | assert order.static_price == "2342.23" 113 | 114 | 115 | def test_post_market_order(): 116 | from buycoins import p2p, orders 117 | 118 | _mock_gql(place_market_order_response) 119 | 120 | order = p2p.place_market_order( 121 | side="sell", coin_amount=0.0123, cryptocurrency="ethereum" 122 | ) 123 | assert isinstance(order, orders.OrderType) 124 | assert order.id == "20" 125 | assert order.side == "sell" 126 | assert order.cryptocurrency == "ethereum" 127 | assert order.price_type is None 128 | assert order.dynamic_exchange_rate is None 129 | assert order.static_price is None 130 | 131 | 132 | def test_get_orders(): 133 | from buycoins import p2p 134 | from buycoins.modules.orders import OrderType 135 | 136 | _mock_gql(get_orders_response) 137 | orders, price_expiry = p2p.get_orders(status="active") 138 | 139 | assert price_expiry == "2340782074" 140 | assert 3 == len(orders) 141 | assert orders[0].cryptocurrency == "bitcoin" 142 | assert isinstance(orders[0], OrderType) 143 | assert all(order.status == "active" for order in orders) 144 | 145 | 146 | def test_get_market_book(): 147 | from buycoins import p2p 148 | from buycoins.modules.orders import OrderType 149 | 150 | _mock_gql(get_market_book_response) 151 | 152 | orders, price_expiry = p2p.get_market_book() 153 | assert price_expiry == "2340782074" 154 | assert 2 == len(orders) 155 | assert isinstance(orders[0], OrderType) 156 | -------------------------------------------------------------------------------- /tests/test_transactions.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from tests.utils import _mock_gql 4 | 5 | 6 | get_balances_response = dict( 7 | getBalances=[ 8 | dict(id="1", cryptocurrency="bitcoin", confirmedBalance="10.01"), 9 | dict(id="2", cryptocurrency="ethereum", confirmedBalance="2.304"), 10 | dict(id="3", cryptocurrency="litecoin", confirmedBalance="0.0233"), 11 | ] 12 | ) 13 | 14 | estimate_network_fee_response = dict( 15 | getEstimatedNetworkFee=dict(estimatedFee="2322.3", total="2342.02") 16 | ) 17 | 18 | 19 | send_crypto_response = dict( 20 | send=dict( 21 | id="1", 22 | address="addr", 23 | amount="0.3222", 24 | cryptocurrency="bitcoin", 25 | fee="0.0002", 26 | status="active", 27 | transaction=dict(id="tx1", txhash="a49s038aty2sfa23434872"), 28 | ) 29 | ) 30 | 31 | 32 | create_address_response = dict( 33 | createAddress=dict(cryptocurrency="litecoin", address="addr1") 34 | ) 35 | 36 | 37 | def test_get_balances(): 38 | from buycoins import transactions 39 | 40 | _mock_gql(get_balances_response) 41 | 42 | balances = transactions.get_balances() 43 | assert 3 == len(balances) 44 | assert all(isinstance(bal, transactions.CoinBalanceType) for bal in balances) 45 | assert balances[0].id == "1" 46 | assert balances[0].cryptocurrency == "bitcoin" 47 | assert balances[0].confirmed_balance == Decimal("10.01") 48 | 49 | 50 | def test_get_balance(): 51 | from buycoins import transactions 52 | 53 | _mock_gql(get_balances_response) 54 | 55 | balance = transactions.get_balance("litecoin") 56 | assert isinstance(balance, transactions.CoinBalanceType) 57 | assert balance.cryptocurrency == "litecoin" 58 | assert balance.id == "3" 59 | 60 | 61 | def test_estimate_network_fees(): 62 | from buycoins import transactions 63 | 64 | _mock_gql(estimate_network_fee_response) 65 | 66 | fee = transactions.estimate_network_fee("bitcoin", 0.0234) 67 | assert isinstance(fee, transactions.NetworkFeeType) 68 | assert fee.estimated_fee == Decimal("2322.3") 69 | assert fee.total == Decimal("2342.02") 70 | 71 | 72 | def test_send_cryptocurrency(): 73 | from buycoins import transactions 74 | 75 | _mock_gql(send_crypto_response) 76 | 77 | sent = transactions.send(cryptocurrency="bitcoin", amount=0.3222, address="addr") 78 | assert isinstance(sent, transactions.SendReturnValueType) 79 | assert sent.id == "1" 80 | assert sent.cryptocurrency == "bitcoin" 81 | assert sent.amount == Decimal("0.3222") 82 | assert sent.transaction.id == "tx1" 83 | 84 | 85 | def test_create_address(): 86 | from buycoins import transactions 87 | 88 | _mock_gql(create_address_response) 89 | 90 | created_address = transactions.create_address("litecoin") 91 | assert isinstance(created_address, transactions.AddressType) 92 | assert created_address.address == "addr1" 93 | assert created_address.cryptocurrency == "litecoin" 94 | -------------------------------------------------------------------------------- /tests/test_webhook.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def test_verify_webhook_payload(): 5 | from buycoins import webhook 6 | 7 | webhook_token = "webhook-token" 8 | body = json.dumps( 9 | dict( 10 | hook_id=10, 11 | hook_key="d814153e-2ced-4d81-82f1-327d000a3ca2", 12 | payload=dict(event="coins.incoming"), 13 | ) 14 | ).encode("utf-8") 15 | header_signature = "4d9db82c8f4fa74f9282c694b7780b80ab466a6f" 16 | 17 | assert webhook.verify_payload(body, webhook_token, header_signature) 18 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict 3 | 4 | import httpretty 5 | from buycoins.client import BUYCOINS_GRAPHQL_ENDPOINT 6 | 7 | 8 | def _make_graphql_response(resp: dict): 9 | return json.dumps(dict(data=resp)) 10 | 11 | 12 | def _mock_gql(response: Dict): 13 | httpretty.register_uri( 14 | httpretty.POST, 15 | BUYCOINS_GRAPHQL_ENDPOINT, 16 | status=200, 17 | body=_make_graphql_response(response), 18 | ) 19 | --------------------------------------------------------------------------------