├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── release.yaml
└── workflows
│ ├── ci.yml
│ ├── conventional-label.yaml
│ └── publish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── Makefile
├── README.md
├── docs
├── cover.png
└── webhook-listener.png
├── poetry.lock
├── pyproject.toml
├── requirements.txt
├── telepay
├── __init__.py
└── v1
│ ├── __init__.py
│ ├── _async
│ ├── __init__.py
│ └── client.py
│ ├── _sync
│ ├── __init__.py
│ └── client.py
│ ├── auth.py
│ ├── errors.py
│ ├── http_clients.py
│ ├── models
│ ├── __init__.py
│ ├── account.py
│ ├── assets.py
│ ├── invoice.py
│ ├── wallets.py
│ └── webhooks.py
│ ├── utils.py
│ └── webhooks.py
└── tests
├── __init__.py
└── v1
├── __init__.py
├── _async
├── __init__.py
└── test_client.py
├── _sync
├── __init__.py
└── test_client.py
└── utils.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Desktop (please complete the following information):**
28 |
29 | * OS: [e.g. iOS]
30 | * Browser [e.g. chrome, safari]
31 | * Version [e.g. 22]
32 |
33 | **Smartphone (please complete the following information):**
34 |
35 | * Device: [e.g. iPhone6]
36 | * OS: [e.g. iOS8.1]
37 | * Browser [e.g. stock browser, safari]
38 | * Version [e.g. 22]
39 |
40 | **Additional context**
41 | Add any other context about the problem here.
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'enhancement'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | target-branch: "develop"
8 |
--------------------------------------------------------------------------------
/.github/release.yaml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | labels:
4 | - ignore-for-release
5 | authors:
6 | - octocat
7 | categories:
8 | - title: Breaking Changes 🛠
9 | labels:
10 | - breaking
11 | - title: Exciting New Features 🎉
12 | labels:
13 | - feature
14 | - title: Fixes 🔧
15 | labels:
16 | - fix
17 | - title: Other Changes
18 | labels:
19 | - "*"
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [ push, pull_request ]
3 | jobs:
4 | CI:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | max-parallel: 4
8 | matrix:
9 | python-version: ["3.10"]
10 | poetry-version: ["1.3.2"]
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | ref: ${{ github.event.pull_request.head.sha }}
16 | - name: Set up Python ${{ matrix.python-version }}
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | - name: Set up Poetry
21 | uses: abatilo/actions-poetry@v2
22 | with:
23 | poetry-version: ${{ matrix.poetry-version }}
24 | - name: Run tests
25 | env:
26 | TELEPAY_SECRET_API_KEY: ${{ secrets.TELEPAY_SECRET_API_KEY }}
27 | run: make tests
28 | - name: Upload coverage
29 | uses: codecov/codecov-action@v1
30 |
--------------------------------------------------------------------------------
/.github/workflows/conventional-label.yaml:
--------------------------------------------------------------------------------
1 | # Warning, do not check out untrusted code with
2 | # the pull_request_target event.
3 | on:
4 | pull_request_target:
5 | types: [ opened, edited ]
6 | name: conventional-release-labels
7 | jobs:
8 | label:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: bcoe/conventional-release-labels@v1
12 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Publish
3 | on:
4 | push:
5 | tags: v*.*.*
6 | jobs:
7 | Publish:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | max-parallel: 1
11 | matrix:
12 | python-version: ["3.10"]
13 | poetry-version: ["1.3.2"]
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: Set up Python
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | - name: Set up Poetry
21 | uses: abatilo/actions-poetry@v2
22 | with:
23 | poetry-version: ${{ matrix.poetry-version }}
24 | - name: Run tests
25 | env:
26 | TELEPAY_SECRET_API_KEY: ${{ secrets.TELEPAY_SECRET_API_KEY }}
27 | run: make tests
28 | - name: Run build
29 | run: poetry build
30 | - name: Publish in GitHub Releases
31 | uses: svenstaro/upload-release-action@v2
32 | with:
33 | repo_token: ${{ secrets.GITHUB_TOKEN }}
34 | file: dist/*
35 | tag: ${{ github.ref }}
36 | overwrite: true
37 | file_glob: true
38 | - name: Pyblish to PyPI
39 | uses: pypa/gh-action-pypi-publish@release/v1
40 | with:
41 | password: ${{ secrets.PYPI_API_TOKEN }}
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Jetbrains IDE
132 | .idea/
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 |
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v4.0.1
5 | hooks:
6 | - id: trailing-whitespace
7 | - id: check-added-large-files
8 | - id: mixed-line-ending
9 | args: ['--fix=lf']
10 |
11 | - repo: https://github.com/pre-commit/mirrors-isort
12 | rev: v5.8.0
13 | hooks:
14 | - id: isort
15 | args: ['--multi-line=3', '--trailing-comma', '--force-grid-wrap=0', '--use-parentheses', '--line-width=88']
16 |
17 | - repo: https://github.com/humitos/mirrors-autoflake.git
18 | rev: v1.1
19 | hooks:
20 | - id: autoflake
21 | args: ['--in-place', '--remove-all-unused-imports']
22 |
23 | # - repo: https://github.com/ambv/black
24 | # rev: 21.5b1
25 | # hooks:
26 | # - id: black
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 TelePay.cash
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.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | poetry install
3 |
4 | tests: install
5 | poetry run flake8 . --count --show-source --statistics --max-line-length=88 --extend-ignore=E203 --exclude=.venv
6 | poetry run black . --check
7 | poetry run isort . --profile=black
8 | poetry run pre-commit run --all-files
9 | poetry run pytest -v
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python SDK for the TelePay API
2 |
3 | 
4 |
5 | Official TelePay client library for the Python language, so you can easily process cryptocurrency payments using the REST API.
6 |
7 | [](https://opensource.org/licenses/MIT)
8 | [](https://github.com/telepay-cash/telepay-python/actions?query=workflow%3ACI)
9 | [](https://pypi.org/project/telepay)
10 | [](https://github.com/telepay-cash/telepay-python/commits)
11 | [](https://github.com/telepay-cash/telepay-python/commits)
12 | [](https://github.com/telepay-cash/telepay-python/stargazers)
13 | [](https://github.com/telepay-cash/telepay-python/network/members)
14 | [](https://github.com/telepay-cash/telepay-python)
15 | [](https://github.com/telepay-cash/telepay-python/graphs/contributors)
16 | [](https://pepy.tech/project/telepay)
17 | [](https://github.com/psf/black)
18 | [](https://pycqa.github.io/isort/)
19 | [](https://t.me/TelePayCash)
20 | [](https://blog.telepay.cash)
21 |
22 | ## Installation
23 |
24 | Install the package with pip:
25 |
26 | ```bash
27 | pip install telepay
28 | ```
29 |
30 | Or using [Poetry](https://python-poetry.org/):
31 |
32 | ```bash
33 | poetry add telepay
34 | ```
35 |
36 | ## Using the library
37 |
38 | Refer to the [TelePay Docs](https://telepay.readme.io) and follow the [first steps guide](https://telepay.readme.io/reference/first-steps), you'll get your TelePay account and API key.
39 |
40 | To make requests to the TelePay API, you need to import a client. We have two clients:
41 | * `TelePaySyncClient`: make requests synchronously.
42 | * `TelePayAsyncClient` make requests asynchronously.
43 |
44 | **Import and use the client**
45 |
46 | ```python
47 | from telepay.v1 import TelePaySyncClient, TelePayAsyncClient
48 |
49 | client = TelePaySyncClient(secret_api_key)
50 | client = TelePayAsyncClient(secret_api_key)
51 | ```
52 |
53 | **Use the client as a context manager**
54 |
55 | We recommend using the client as a context manager, like this:
56 |
57 | ```python
58 | with TelePaySyncClient(secret_api_key) as client:
59 | # use the client
60 | ...
61 | ```
62 |
63 | or
64 |
65 | ```python
66 | async with TelePayAsyncClient(secret_api_key) as client:
67 | # use the client
68 | ...
69 | ```
70 |
71 | ## API endpoints
72 |
73 | The API endpoints are documented in the [TelePay documentation](https://telepay.readme.io/reference/endpoints), refer to that pages to know more about them.
74 |
75 | To manage the requests, if the client is async, you should use the `await` keyword, like this:
76 |
77 | ```python
78 | response = await client.method(...)
79 | ```
80 |
81 | Where `method` is the endpoint method.
82 |
83 | **get_me**
84 |
85 | Info about the current merchant. [Read docs](https://telepay.readme.io/reference/getme)
86 |
87 | ```python
88 | account = client.get_me()
89 | ```
90 |
91 | **get_balance**
92 |
93 | Get your merchant wallet assets with corresponding balance. [Read docs](https://telepay.readme.io/reference/getbalance)
94 |
95 | ```python
96 | wallets = client.get_balance()
97 | ```
98 |
99 | Or get a specific wallet balance by specifying the `asset`, `blockchain` and `network`.
100 |
101 | ```python
102 | wallet = client.get_balance(asset='TON', blockchain='TON', network='network')
103 | ```
104 |
105 | ### Assets
106 |
107 | **get_asset**
108 |
109 | Get asset details. [Read docs](https://telepay.readme.io/reference/getasset)
110 |
111 | ```python
112 | asset = client.get_asset(asset='TON', blockchain='TON')
113 | ```
114 |
115 | **get_assets**
116 |
117 | Get assets suported by TelePay. [Read docs](https://telepay.readme.io/reference/getassets)
118 |
119 | ```python
120 | assets = client.get_assets()
121 | ```
122 |
123 | ### Invoices
124 |
125 | **get_invoice**
126 |
127 | Get invoice details, by its number. [Read docs](https://telepay.readme.io/reference/getinvoice)
128 |
129 | ```python
130 | invoice = client.get_invoice(number)
131 | ```
132 |
133 | **get_invoices**
134 |
135 | Get your merchant invoices. [Read docs](https://telepay.readme.io/reference/getinvoices)
136 |
137 | ```python
138 | invoices = client.get_invoices()
139 | ```
140 |
141 | **create_invoice**
142 |
143 | Creates an invoice, associated to your merchant. [Read docs](https://telepay.readme.io/reference/createinvoice)
144 |
145 | ```python
146 | invoice = client.create_invoice(
147 | asset='TON',
148 | blockchain='TON',
149 | network='mainnet',
150 | amount=1,
151 | description='Product',
152 | metadata={
153 | 'color': 'red',
154 | 'size': 'large',
155 | },
156 | success_url='https://example.com/success',
157 | cancel_url='https://example.com/cancel',
158 | expires_at=30
159 | )
160 | ```
161 |
162 | **cancel_invoice**
163 |
164 | Cancel invoice, by its number. [Read docs](https://telepay.readme.io/reference/cancelinvoice)
165 |
166 | ```python
167 | invoice = client.cancel_invoice(number)
168 | ```
169 |
170 | **delete_invoice**
171 |
172 | Delete invoice, by its number. [Read docs](https://telepay.readme.io/reference/deleteinvoice)
173 |
174 | ```python
175 | status = client.delete_invoice(number)
176 | ```
177 |
178 | ### Transfers
179 |
180 | **transfer**
181 |
182 | Transfer funds between internal wallets. Off-chain operation. [Read docs](https://telepay.readme.io/reference/transfer)
183 |
184 | ```python
185 | status = client.transfer(
186 | asset='TON',
187 | blockchain='TON',
188 | network='mainnet',
189 | amount=1,
190 | username='test',
191 | message='Thanks'
192 | )
193 | ```
194 |
195 | ### Withdrawals
196 |
197 | **get_withdraw_minimum**
198 |
199 | Obtains minimum amount required to withdraw funds on a given asset. [Read docs](https://telepay.readme.io/reference/getwithdrawminimum)
200 |
201 | ```python
202 | minimum = client.get_withdraw_minimum(
203 | asset='TON',
204 | blockchain='TON',
205 | network='mainnet',
206 | )
207 | ```
208 |
209 | **get_withdraw_fee**
210 |
211 | Get estimated withdraw fee, composed of blockchain fee and processing fee. [Read docs](https://telepay.readme.io/reference/getwithdrawfee)
212 |
213 | ```python
214 | fees = client.get_withdraw_fee(
215 | to_address='EQCKYK7bYBt1t8UmdhImrbiSzC5ijfo_H3Zc_Hk8ksRpOkOk',
216 | asset='TON',
217 | blockchain='TON',
218 | network='mainnet',
219 | amount=1,
220 | message='test'
221 | )
222 | ```
223 |
224 | **withdraw**
225 |
226 | Withdraw funds from merchant wallet to external wallet. On-chain operation. [Read docs](https://telepay.readme.io/reference/withdraw)
227 |
228 | ```python
229 | status = client.withdraw(
230 | to_address='EQCKYK7bYBt1t8UmdhImrbiSzC5ijfo_H3Zc_Hk8ksRpOkOk',
231 | asset='TON',
232 | blockchain='TON',
233 | network='mainnet',
234 | amount=1,
235 | message='test'
236 | )
237 | ```
238 |
239 | ### Webhooks
240 |
241 | > Webhooks allows you to get updates delivered to your app server whenever we have events on our side. We will send a POST request over HTTPS, with serialized JSON data of the event, to the endpoint defined by you in the Developers > Webhooks section, in your merchant dashboard. [Read more in the docs](https://telepay.readme.io/reference/webhooks).
242 |
243 | **get_webhook**
244 |
245 | Get webhook details, by its id. [Read docs](https://telepay.readme.io/reference/getwebhook)
246 |
247 | ```python
248 | client.get_webhook(id)
249 | ```
250 |
251 | **get_webhooks**
252 |
253 | Get your merchant webhooks. [Read docs](https://telepay.readme.io/reference/getwebhooks)
254 |
255 | ```python
256 | webhooks = client.get_webhooks()
257 | ```
258 |
259 | **create_webhook**
260 |
261 | Create a webhook. [Read docs](https://telepay.readme.io/reference/createwebhook)
262 |
263 | ```python
264 | webhook = client.create_webhook(
265 | url='https://example.com',
266 | secret='hello',
267 | events=['all'],
268 | active=True
269 | )
270 | ```
271 |
272 | **update_webhook**
273 |
274 | Update a webhook. [Read docs](https://telepay.readme.io/reference/updatewebhook)
275 |
276 | ```python
277 | webhook = client.update_webhook(
278 | url='https://example.com',
279 | secret='hello',
280 | events=['invoice.completed'],
281 | active=True
282 | )
283 | ```
284 |
285 | **activate_webhook**
286 |
287 | Activate a webhook, by its id. [Read docs](https://telepay.readme.io/reference/activatewebhook)
288 |
289 | ```python
290 | client.activate_webhook(id)
291 | ```
292 |
293 | **deactivate_webhook**
294 |
295 | Deactivate a webhook, by its id. [Read docs](https://telepay.readme.io/reference/deactivatewebhook)
296 |
297 | ```python
298 | client.deactivate_webhook(id)
299 | ```
300 |
301 | **delete_webhook**
302 |
303 | Delete a webhook, by its id. [Read docs](https://telepay.readme.io/reference/deletewebhook)
304 |
305 | ```python
306 | client.deactivate_webhook(id)
307 | ```
308 |
309 | ### Webhook utils
310 |
311 | The `telepay.v1.webhooks` module contains utilities to manage webhooks received on your side.
312 |
313 | * `get_signature(data, secret)`: Returns a webhook signature, used to verify data integrity of the webhook. This is optional, but highly recommended.
314 | * `TelePayWebhookListener`: A lightweight webhook listener, to receive webhooks from TelePay. You could build your own, like using django views, flask views, or any other web framework. Your choice.
315 |
316 | **Example using `TelePayWebhookListener`**
317 |
318 | ```python
319 | from telepay.v1.webhooks import TelePayWebhookListener
320 |
321 | def callback(headers, data):
322 | print("Executing callback...")
323 | # do your stuff here
324 |
325 | listener = TelePayWebhookListener(
326 | secret="SECRET",
327 | callback=callback,
328 | host="localhost",
329 | port=5000,
330 | url="/webhook",
331 | log_level="error",
332 | )
333 |
334 | listener.listen()
335 | ```
336 |
337 | Running the listener will output something like this:
338 |
339 | 
340 |
341 | Modify the listener parameters to your needs, knowing this:
342 | * `secret`: A secret token that your side knows and it's configured in the webhook definition, on the TelePay dashboard.
343 | * `callback`: The callback function that is called when new webhook arrives, receiving it's HTTP headers and data.
344 | * `host`: The host on which the listener will be running.
345 | * `port`: The port on which the listener will be exposed.
346 | * `url`: The webhook url, which is secret and should only be known by your app and TelePay. Otherwise, it could lead to security issues.
347 | * `log_level`: The listener logger level, like `"error"`, `"info"` or `"debug"`.
348 |
349 | ## Contributors ✨
350 |
351 | The library is made by ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
352 |
353 |
354 |
355 |
356 |
384 |
385 |
386 |
387 |
388 |
389 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
390 |
--------------------------------------------------------------------------------
/docs/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/docs/cover.png
--------------------------------------------------------------------------------
/docs/webhook-listener.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/docs/webhook-listener.png
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "anyio"
5 | version = "3.6.2"
6 | description = "High level compatibility layer for multiple asynchronous event loop implementations"
7 | category = "main"
8 | optional = false
9 | python-versions = ">=3.6.2"
10 | files = [
11 | {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
12 | {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
13 | ]
14 |
15 | [package.dependencies]
16 | idna = ">=2.8"
17 | sniffio = ">=1.1"
18 | trio = {version = ">=0.16,<0.22", optional = true, markers = "extra == \"trio\""}
19 |
20 | [package.extras]
21 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
22 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
23 | trio = ["trio (>=0.16,<0.22)"]
24 |
25 | [[package]]
26 | name = "asgiref"
27 | version = "3.6.0"
28 | description = "ASGI specs, helper code, and adapters"
29 | category = "main"
30 | optional = false
31 | python-versions = ">=3.7"
32 | files = [
33 | {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"},
34 | {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"},
35 | ]
36 |
37 | [package.extras]
38 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
39 |
40 | [[package]]
41 | name = "async-generator"
42 | version = "1.10"
43 | description = "Async generators and context managers for Python 3.5+"
44 | category = "dev"
45 | optional = false
46 | python-versions = ">=3.5"
47 | files = [
48 | {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"},
49 | {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"},
50 | ]
51 |
52 | [[package]]
53 | name = "atomicwrites"
54 | version = "1.4.1"
55 | description = "Atomic file writes."
56 | category = "dev"
57 | optional = false
58 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
59 | files = [
60 | {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
61 | ]
62 |
63 | [[package]]
64 | name = "attrs"
65 | version = "22.2.0"
66 | description = "Classes Without Boilerplate"
67 | category = "dev"
68 | optional = false
69 | python-versions = ">=3.6"
70 | files = [
71 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
72 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
73 | ]
74 |
75 | [package.extras]
76 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
77 | dev = ["attrs[docs,tests]"]
78 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
79 | tests = ["attrs[tests-no-zope]", "zope.interface"]
80 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
81 |
82 | [[package]]
83 | name = "black"
84 | version = "22.12.0"
85 | description = "The uncompromising code formatter."
86 | category = "dev"
87 | optional = false
88 | python-versions = ">=3.7"
89 | files = [
90 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
91 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
92 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
93 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
94 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
95 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
96 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
97 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
98 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
99 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
100 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
101 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
102 | ]
103 |
104 | [package.dependencies]
105 | click = ">=8.0.0"
106 | mypy-extensions = ">=0.4.3"
107 | pathspec = ">=0.9.0"
108 | platformdirs = ">=2"
109 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
110 |
111 | [package.extras]
112 | colorama = ["colorama (>=0.4.3)"]
113 | d = ["aiohttp (>=3.7.4)"]
114 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
115 | uvloop = ["uvloop (>=0.15.2)"]
116 |
117 | [[package]]
118 | name = "certifi"
119 | version = "2022.12.7"
120 | description = "Python package for providing Mozilla's CA Bundle."
121 | category = "main"
122 | optional = false
123 | python-versions = ">=3.6"
124 | files = [
125 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
126 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
127 | ]
128 |
129 | [[package]]
130 | name = "cffi"
131 | version = "1.15.1"
132 | description = "Foreign Function Interface for Python calling C code."
133 | category = "dev"
134 | optional = false
135 | python-versions = "*"
136 | files = [
137 | {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
138 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
139 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
140 | {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
141 | {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
142 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
143 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
144 | {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
145 | {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
146 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
147 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
148 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
149 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
150 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
151 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
152 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
153 | {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
154 | {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
155 | {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
156 | {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
157 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
158 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
159 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
160 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
161 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
162 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
163 | {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
164 | {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
165 | {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
166 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
167 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
168 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
169 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
170 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
171 | {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
172 | {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
173 | {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
174 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
175 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
176 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
177 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
178 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
179 | {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
180 | {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
181 | {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
182 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
183 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
184 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
185 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
186 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
187 | {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
188 | {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
189 | {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
190 | {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
191 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
192 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
193 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
194 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
195 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
196 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
197 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
198 | {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
199 | {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
200 | {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
201 | ]
202 |
203 | [package.dependencies]
204 | pycparser = "*"
205 |
206 | [[package]]
207 | name = "cfgv"
208 | version = "3.3.1"
209 | description = "Validate configuration and produce human readable error messages."
210 | category = "dev"
211 | optional = false
212 | python-versions = ">=3.6.1"
213 | files = [
214 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
215 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
216 | ]
217 |
218 | [[package]]
219 | name = "click"
220 | version = "8.1.3"
221 | description = "Composable command line interface toolkit"
222 | category = "main"
223 | optional = false
224 | python-versions = ">=3.7"
225 | files = [
226 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
227 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
228 | ]
229 |
230 | [package.dependencies]
231 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
232 |
233 | [[package]]
234 | name = "colorama"
235 | version = "0.4.6"
236 | description = "Cross-platform colored terminal text."
237 | category = "main"
238 | optional = false
239 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
240 | files = [
241 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
242 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
243 | ]
244 |
245 | [[package]]
246 | name = "distlib"
247 | version = "0.3.6"
248 | description = "Distribution utilities"
249 | category = "dev"
250 | optional = false
251 | python-versions = "*"
252 | files = [
253 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
254 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
255 | ]
256 |
257 | [[package]]
258 | name = "fastapi"
259 | version = "0.75.2"
260 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
261 | category = "main"
262 | optional = false
263 | python-versions = ">=3.6.1"
264 | files = [
265 | {file = "fastapi-0.75.2-py3-none-any.whl", hash = "sha256:a70d31f4249b6b42dbe267667d22f83af645b2d857876c97f83ca9573215784f"},
266 | {file = "fastapi-0.75.2.tar.gz", hash = "sha256:b5dac161ee19d33346040d3f44d8b7a9ac09b37df9efff95891f5e7641fa482f"},
267 | ]
268 |
269 | [package.dependencies]
270 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
271 | starlette = "0.17.1"
272 |
273 | [package.extras]
274 | all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
275 | dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
276 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
277 | test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
278 |
279 | [[package]]
280 | name = "filelock"
281 | version = "3.9.0"
282 | description = "A platform independent file lock."
283 | category = "dev"
284 | optional = false
285 | python-versions = ">=3.7"
286 | files = [
287 | {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
288 | {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
289 | ]
290 |
291 | [package.extras]
292 | docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
293 | testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
294 |
295 | [[package]]
296 | name = "flake8"
297 | version = "4.0.1"
298 | description = "the modular source code checker: pep8 pyflakes and co"
299 | category = "dev"
300 | optional = false
301 | python-versions = ">=3.6"
302 | files = [
303 | {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
304 | {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
305 | ]
306 |
307 | [package.dependencies]
308 | mccabe = ">=0.6.0,<0.7.0"
309 | pycodestyle = ">=2.8.0,<2.9.0"
310 | pyflakes = ">=2.4.0,<2.5.0"
311 |
312 | [[package]]
313 | name = "h11"
314 | version = "0.14.0"
315 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
316 | category = "main"
317 | optional = false
318 | python-versions = ">=3.7"
319 | files = [
320 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
321 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
322 | ]
323 |
324 | [[package]]
325 | name = "httpcore"
326 | version = "0.16.3"
327 | description = "A minimal low-level HTTP client."
328 | category = "main"
329 | optional = false
330 | python-versions = ">=3.7"
331 | files = [
332 | {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
333 | {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
334 | ]
335 |
336 | [package.dependencies]
337 | anyio = ">=3.0,<5.0"
338 | certifi = "*"
339 | h11 = ">=0.13,<0.15"
340 | sniffio = ">=1.0.0,<2.0.0"
341 |
342 | [package.extras]
343 | http2 = ["h2 (>=3,<5)"]
344 | socks = ["socksio (>=1.0.0,<2.0.0)"]
345 |
346 | [[package]]
347 | name = "httpx"
348 | version = "0.23.3"
349 | description = "The next generation HTTP client."
350 | category = "main"
351 | optional = false
352 | python-versions = ">=3.7"
353 | files = [
354 | {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
355 | {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
356 | ]
357 |
358 | [package.dependencies]
359 | certifi = "*"
360 | httpcore = ">=0.15.0,<0.17.0"
361 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
362 | sniffio = "*"
363 |
364 | [package.extras]
365 | brotli = ["brotli", "brotlicffi"]
366 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
367 | http2 = ["h2 (>=3,<5)"]
368 | socks = ["socksio (>=1.0.0,<2.0.0)"]
369 |
370 | [[package]]
371 | name = "identify"
372 | version = "2.5.15"
373 | description = "File identification library for Python"
374 | category = "dev"
375 | optional = false
376 | python-versions = ">=3.7"
377 | files = [
378 | {file = "identify-2.5.15-py2.py3-none-any.whl", hash = "sha256:1f4b36c5f50f3f950864b2a047308743f064eaa6f6645da5e5c780d1c7125487"},
379 | {file = "identify-2.5.15.tar.gz", hash = "sha256:c22aa206f47cc40486ecf585d27ad5f40adbfc494a3fa41dc3ed0499a23b123f"},
380 | ]
381 |
382 | [package.extras]
383 | license = ["ukkonen"]
384 |
385 | [[package]]
386 | name = "idna"
387 | version = "3.4"
388 | description = "Internationalized Domain Names in Applications (IDNA)"
389 | category = "main"
390 | optional = false
391 | python-versions = ">=3.5"
392 | files = [
393 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
394 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
395 | ]
396 |
397 | [[package]]
398 | name = "iniconfig"
399 | version = "2.0.0"
400 | description = "brain-dead simple config-ini parsing"
401 | category = "dev"
402 | optional = false
403 | python-versions = ">=3.7"
404 | files = [
405 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
406 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
407 | ]
408 |
409 | [[package]]
410 | name = "isort"
411 | version = "5.11.4"
412 | description = "A Python utility / library to sort Python imports."
413 | category = "dev"
414 | optional = false
415 | python-versions = ">=3.7.0"
416 | files = [
417 | {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
418 | {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
419 | ]
420 |
421 | [package.extras]
422 | colors = ["colorama (>=0.4.3,<0.5.0)"]
423 | pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
424 | plugins = ["setuptools"]
425 | requirements-deprecated-finder = ["pip-api", "pipreqs"]
426 |
427 | [[package]]
428 | name = "mccabe"
429 | version = "0.6.1"
430 | description = "McCabe checker, plugin for flake8"
431 | category = "dev"
432 | optional = false
433 | python-versions = "*"
434 | files = [
435 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
436 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
437 | ]
438 |
439 | [[package]]
440 | name = "mypy-extensions"
441 | version = "0.4.3"
442 | description = "Experimental type system extensions for programs checked with the mypy typechecker."
443 | category = "dev"
444 | optional = false
445 | python-versions = "*"
446 | files = [
447 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
448 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
449 | ]
450 |
451 | [[package]]
452 | name = "nodeenv"
453 | version = "1.7.0"
454 | description = "Node.js virtual environment builder"
455 | category = "dev"
456 | optional = false
457 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
458 | files = [
459 | {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
460 | {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
461 | ]
462 |
463 | [package.dependencies]
464 | setuptools = "*"
465 |
466 | [[package]]
467 | name = "outcome"
468 | version = "1.2.0"
469 | description = "Capture the outcome of Python function calls."
470 | category = "dev"
471 | optional = false
472 | python-versions = ">=3.7"
473 | files = [
474 | {file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"},
475 | {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
476 | ]
477 |
478 | [package.dependencies]
479 | attrs = ">=19.2.0"
480 |
481 | [[package]]
482 | name = "packaging"
483 | version = "23.0"
484 | description = "Core utilities for Python packages"
485 | category = "dev"
486 | optional = false
487 | python-versions = ">=3.7"
488 | files = [
489 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
490 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
491 | ]
492 |
493 | [[package]]
494 | name = "pathspec"
495 | version = "0.10.3"
496 | description = "Utility library for gitignore style pattern matching of file paths."
497 | category = "dev"
498 | optional = false
499 | python-versions = ">=3.7"
500 | files = [
501 | {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"},
502 | {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"},
503 | ]
504 |
505 | [[package]]
506 | name = "platformdirs"
507 | version = "2.6.2"
508 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
509 | category = "dev"
510 | optional = false
511 | python-versions = ">=3.7"
512 | files = [
513 | {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
514 | {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
515 | ]
516 |
517 | [package.extras]
518 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
519 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
520 |
521 | [[package]]
522 | name = "pluggy"
523 | version = "1.0.0"
524 | description = "plugin and hook calling mechanisms for python"
525 | category = "dev"
526 | optional = false
527 | python-versions = ">=3.6"
528 | files = [
529 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
530 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
531 | ]
532 |
533 | [package.extras]
534 | dev = ["pre-commit", "tox"]
535 | testing = ["pytest", "pytest-benchmark"]
536 |
537 | [[package]]
538 | name = "pre-commit"
539 | version = "2.21.0"
540 | description = "A framework for managing and maintaining multi-language pre-commit hooks."
541 | category = "dev"
542 | optional = false
543 | python-versions = ">=3.7"
544 | files = [
545 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
546 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
547 | ]
548 |
549 | [package.dependencies]
550 | cfgv = ">=2.0.0"
551 | identify = ">=1.0.0"
552 | nodeenv = ">=0.11.1"
553 | pyyaml = ">=5.1"
554 | virtualenv = ">=20.10.0"
555 |
556 | [[package]]
557 | name = "py"
558 | version = "1.11.0"
559 | description = "library with cross-python path, ini-parsing, io, code, log facilities"
560 | category = "dev"
561 | optional = false
562 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
563 | files = [
564 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
565 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
566 | ]
567 |
568 | [[package]]
569 | name = "pycodestyle"
570 | version = "2.8.0"
571 | description = "Python style guide checker"
572 | category = "dev"
573 | optional = false
574 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
575 | files = [
576 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
577 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
578 | ]
579 |
580 | [[package]]
581 | name = "pycparser"
582 | version = "2.21"
583 | description = "C parser in Python"
584 | category = "dev"
585 | optional = false
586 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
587 | files = [
588 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
589 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
590 | ]
591 |
592 | [[package]]
593 | name = "pydantic"
594 | version = "1.10.4"
595 | description = "Data validation and settings management using python type hints"
596 | category = "main"
597 | optional = false
598 | python-versions = ">=3.7"
599 | files = [
600 | {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"},
601 | {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"},
602 | {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"},
603 | {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"},
604 | {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"},
605 | {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"},
606 | {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"},
607 | {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"},
608 | {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"},
609 | {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"},
610 | {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"},
611 | {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"},
612 | {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"},
613 | {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"},
614 | {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"},
615 | {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"},
616 | {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"},
617 | {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"},
618 | {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"},
619 | {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"},
620 | {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"},
621 | {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"},
622 | {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"},
623 | {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"},
624 | {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"},
625 | {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"},
626 | {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"},
627 | {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"},
628 | {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"},
629 | {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"},
630 | {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"},
631 | {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"},
632 | {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"},
633 | {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"},
634 | {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"},
635 | {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"},
636 | ]
637 |
638 | [package.dependencies]
639 | typing-extensions = ">=4.2.0"
640 |
641 | [package.extras]
642 | dotenv = ["python-dotenv (>=0.10.4)"]
643 | email = ["email-validator (>=1.0.3)"]
644 |
645 | [[package]]
646 | name = "pyflakes"
647 | version = "2.4.0"
648 | description = "passive checker of Python programs"
649 | category = "dev"
650 | optional = false
651 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
652 | files = [
653 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
654 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
655 | ]
656 |
657 | [[package]]
658 | name = "pytest"
659 | version = "6.2.5"
660 | description = "pytest: simple powerful testing with Python"
661 | category = "dev"
662 | optional = false
663 | python-versions = ">=3.6"
664 | files = [
665 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
666 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
667 | ]
668 |
669 | [package.dependencies]
670 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
671 | attrs = ">=19.2.0"
672 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
673 | iniconfig = "*"
674 | packaging = "*"
675 | pluggy = ">=0.12,<2.0"
676 | py = ">=1.8.2"
677 | toml = "*"
678 |
679 | [package.extras]
680 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
681 |
682 | [[package]]
683 | name = "python-dotenv"
684 | version = "0.20.0"
685 | description = "Read key-value pairs from a .env file and set them as environment variables"
686 | category = "main"
687 | optional = false
688 | python-versions = ">=3.5"
689 | files = [
690 | {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"},
691 | {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"},
692 | ]
693 |
694 | [package.extras]
695 | cli = ["click (>=5.0)"]
696 |
697 | [[package]]
698 | name = "pyyaml"
699 | version = "6.0"
700 | description = "YAML parser and emitter for Python"
701 | category = "dev"
702 | optional = false
703 | python-versions = ">=3.6"
704 | files = [
705 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
706 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
707 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
708 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
709 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
710 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
711 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
712 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
713 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
714 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
715 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
716 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
717 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
718 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
719 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
720 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
721 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
722 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
723 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
724 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
725 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
726 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
727 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
728 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
729 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
730 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
731 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
732 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
733 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
734 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
735 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
736 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
737 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
738 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
739 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
740 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
741 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
742 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
743 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
744 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
745 | ]
746 |
747 | [[package]]
748 | name = "rfc3986"
749 | version = "1.5.0"
750 | description = "Validating URI References per RFC 3986"
751 | category = "main"
752 | optional = false
753 | python-versions = "*"
754 | files = [
755 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
756 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
757 | ]
758 |
759 | [package.dependencies]
760 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
761 |
762 | [package.extras]
763 | idna2008 = ["idna"]
764 |
765 | [[package]]
766 | name = "setuptools"
767 | version = "66.1.1"
768 | description = "Easily download, build, install, upgrade, and uninstall Python packages"
769 | category = "dev"
770 | optional = false
771 | python-versions = ">=3.7"
772 | files = [
773 | {file = "setuptools-66.1.1-py3-none-any.whl", hash = "sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b"},
774 | {file = "setuptools-66.1.1.tar.gz", hash = "sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"},
775 | ]
776 |
777 | [package.extras]
778 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
779 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
780 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
781 |
782 | [[package]]
783 | name = "sniffio"
784 | version = "1.3.0"
785 | description = "Sniff out which async library your code is running under"
786 | category = "main"
787 | optional = false
788 | python-versions = ">=3.7"
789 | files = [
790 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
791 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
792 | ]
793 |
794 | [[package]]
795 | name = "sortedcontainers"
796 | version = "2.4.0"
797 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
798 | category = "dev"
799 | optional = false
800 | python-versions = "*"
801 | files = [
802 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
803 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
804 | ]
805 |
806 | [[package]]
807 | name = "starlette"
808 | version = "0.17.1"
809 | description = "The little ASGI library that shines."
810 | category = "main"
811 | optional = false
812 | python-versions = ">=3.6"
813 | files = [
814 | {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"},
815 | {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"},
816 | ]
817 |
818 | [package.dependencies]
819 | anyio = ">=3.0.0,<4"
820 |
821 | [package.extras]
822 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"]
823 |
824 | [[package]]
825 | name = "toml"
826 | version = "0.10.2"
827 | description = "Python Library for Tom's Obvious, Minimal Language"
828 | category = "dev"
829 | optional = false
830 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
831 | files = [
832 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
833 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
834 | ]
835 |
836 | [[package]]
837 | name = "tomli"
838 | version = "2.0.1"
839 | description = "A lil' TOML parser"
840 | category = "dev"
841 | optional = false
842 | python-versions = ">=3.7"
843 | files = [
844 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
845 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
846 | ]
847 |
848 | [[package]]
849 | name = "trio"
850 | version = "0.21.0"
851 | description = "A friendly Python library for async concurrency and I/O"
852 | category = "dev"
853 | optional = false
854 | python-versions = ">=3.7"
855 | files = [
856 | {file = "trio-0.21.0-py3-none-any.whl", hash = "sha256:4dc0bf9d5cc78767fc4516325b6d80cc0968705a31d0eec2ecd7cdda466265b0"},
857 | {file = "trio-0.21.0.tar.gz", hash = "sha256:523f39b7b69eef73501cebfe1aafd400a9aad5b03543a0eded52952488ff1c13"},
858 | ]
859 |
860 | [package.dependencies]
861 | async-generator = ">=1.9"
862 | attrs = ">=19.2.0"
863 | cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""}
864 | idna = "*"
865 | outcome = "*"
866 | sniffio = "*"
867 | sortedcontainers = "*"
868 |
869 | [[package]]
870 | name = "typing-extensions"
871 | version = "4.4.0"
872 | description = "Backported and Experimental Type Hints for Python 3.7+"
873 | category = "main"
874 | optional = false
875 | python-versions = ">=3.7"
876 | files = [
877 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
878 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
879 | ]
880 |
881 | [[package]]
882 | name = "uvicorn"
883 | version = "0.17.6"
884 | description = "The lightning-fast ASGI server."
885 | category = "main"
886 | optional = false
887 | python-versions = ">=3.7"
888 | files = [
889 | {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"},
890 | {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"},
891 | ]
892 |
893 | [package.dependencies]
894 | asgiref = ">=3.4.0"
895 | click = ">=7.0"
896 | h11 = ">=0.8"
897 |
898 | [package.extras]
899 | standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=10.0)"]
900 |
901 | [[package]]
902 | name = "virtualenv"
903 | version = "20.17.1"
904 | description = "Virtual Python Environment builder"
905 | category = "dev"
906 | optional = false
907 | python-versions = ">=3.6"
908 | files = [
909 | {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"},
910 | {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"},
911 | ]
912 |
913 | [package.dependencies]
914 | distlib = ">=0.3.6,<1"
915 | filelock = ">=3.4.1,<4"
916 | platformdirs = ">=2.4,<3"
917 |
918 | [package.extras]
919 | docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
920 | testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
921 |
922 | [metadata]
923 | lock-version = "2.0"
924 | python-versions = "^3.10"
925 | content-hash = "e98e6dde7a4d27de1250c557b28769d3b8fe88688c25c2c149088924dcd32463"
926 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "telepay"
3 | version = "1.1.0"
4 | description = "Python SDK for the TelePay API"
5 | authors = ["Carlos Lugones", "Reinier Hernández", "Luis Diaz"]
6 | license = "MIT"
7 | readme = "README.md"
8 | homepage = "https://github.com/TelePay-cash/telepay-python"
9 | repository = "https://github.com/TelePay-cash/telepay-python"
10 | keywords = ["payments", "gateway", "cryptocurrencies", "API"]
11 | classifiers = [
12 | "Development Status :: 4 - Beta",
13 | "Environment :: Console",
14 | "Operating System :: OS Independent",
15 | "Topic :: Software Development :: Libraries :: Python Modules",
16 | "Programming Language :: Python",
17 | "Intended Audience :: Developers",
18 | "Natural Language :: English"
19 | ]
20 | include = [
21 | "LICENSE",
22 | ]
23 |
24 | [tool.poetry.urls]
25 | "Bug Tracker" = "https://github.com/TelePay-cash/telepay-python/issues"
26 |
27 | [tool.poetry.dependencies]
28 | python = "^3.10"
29 | httpx = "^0.23.0"
30 | python-dotenv = "^0.20.0"
31 | fastapi = "^0.75.1"
32 | uvicorn = "^0.17.6"
33 | colorama = "^0.4.4"
34 |
35 | [tool.poetry.dev-dependencies]
36 | pytest = "^6.2.5"
37 | anyio = {extras = ["trio"], version = "^3.3.4"}
38 | flake8 = "^4.0.1"
39 | isort = "^5.10.1"
40 | pre-commit = "^2.18.1"
41 | black = "^22.3.0"
42 |
43 | [build-system]
44 | requires = ["poetry-core>=1.0.0"]
45 | build-backend = "poetry.core.masonry.api"
46 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | anyio==3.6.2 ; python_version >= "3.10" and python_version < "4.0"
2 | asgiref==3.6.0 ; python_version >= "3.10" and python_version < "4.0"
3 | certifi==2022.12.7 ; python_version >= "3.10" and python_version < "4.0"
4 | click==8.1.3 ; python_version >= "3.10" and python_version < "4.0"
5 | colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0"
6 | fastapi==0.75.2 ; python_version >= "3.10" and python_version < "4.0"
7 | h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
8 | httpcore==0.16.3 ; python_version >= "3.10" and python_version < "4.0"
9 | httpx==0.23.3 ; python_version >= "3.10" and python_version < "4.0"
10 | idna==3.4 ; python_version >= "3.10" and python_version < "4.0"
11 | pydantic==1.10.4 ; python_version >= "3.10" and python_version < "4.0"
12 | python-dotenv==0.20.0 ; python_version >= "3.10" and python_version < "4.0"
13 | rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "4.0"
14 | sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
15 | starlette==0.17.1 ; python_version >= "3.10" and python_version < "4.0"
16 | typing-extensions==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
17 | uvicorn==0.17.6 ; python_version >= "3.10" and python_version < "4.0"
18 |
--------------------------------------------------------------------------------
/telepay/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.0.2"
2 | __author__ = "Carlos Lugones "
3 | __license__ = "MIT"
4 |
--------------------------------------------------------------------------------
/telepay/v1/__init__.py:
--------------------------------------------------------------------------------
1 | from ._async.client import TelePayAsyncClient # noqa: F401
2 | from ._sync.client import TelePaySyncClient # noqa: F401
3 | from .auth import TelePayAuth # noqa: F401
4 | from .errors import TelePayError # noqa: F401
5 | from .models.account import Account # noqa: F401
6 | from .models.assets import Assets # noqa: F401
7 | from .models.invoice import Invoice # noqa: F401
8 | from .models.wallets import Wallet, Wallets # noqa: F401
9 | from .models.webhooks import Webhook, Webhooks # noqa: F401
10 | from .webhooks import TelePayWebhookListener # noqa: F401
11 |
--------------------------------------------------------------------------------
/telepay/v1/_async/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/telepay/v1/_async/__init__.py
--------------------------------------------------------------------------------
/telepay/v1/_async/client.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass, field
3 | from typing import Union
4 |
5 | from httpx._config import Timeout
6 | from httpx._types import TimeoutTypes
7 |
8 | from ..auth import TelePayAuth
9 | from ..http_clients import AsyncClient
10 | from ..models.account import Account
11 | from ..models.assets import Asset, Assets
12 | from ..models.invoice import Invoice, InvoiceList
13 | from ..models.wallets import Wallet, Wallets
14 | from ..models.webhooks import Webhook, Webhooks
15 | from ..utils import validate_response
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | @dataclass
21 | class TelePayAsyncClient:
22 | """
23 | Creates a TelePay async client.
24 | * API_SECRET: Your merchant private API key.
25 | Any requests without this authentication key will result in error 403.
26 | """
27 |
28 | timeout: TimeoutTypes = field(default=Timeout(60))
29 |
30 | def __init__(self, secret_api_key, timeout=Timeout(60)) -> None:
31 | self.base_url = "https://api.telepay.cash/rest/"
32 | self.timeout = timeout
33 | self.http_client = AsyncClient(
34 | base_url=self.base_url,
35 | headers={"Authorization": secret_api_key},
36 | timeout=self.timeout,
37 | )
38 |
39 | async def __aenter__(self) -> "TelePayAsyncClient":
40 | return self
41 |
42 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
43 | await self.close()
44 |
45 | async def close(self) -> None:
46 | await self.http_client.aclose()
47 |
48 | @staticmethod
49 | def from_auth(auth: TelePayAuth, timeout=Timeout(60)) -> "TelePayAsyncClient":
50 | return TelePayAsyncClient(auth.secret_api_key, timeout=timeout)
51 |
52 | async def get_me(self) -> Account:
53 | """
54 | Info about the current account
55 | """
56 | response = await self.http_client.get("getMe")
57 | logger.debug(f"Response: {response.text}")
58 | validate_response(response)
59 | return Account.from_json(response.json())
60 |
61 | async def get_balance(
62 | self, asset=None, blockchain=None, network=None
63 | ) -> Union[Wallet, Wallets]:
64 | """
65 | Get your merchant wallet assets with corresponding balance
66 | """
67 | if asset and blockchain and network:
68 | response = await self.http_client.post(
69 | "getBalance",
70 | json={"asset": asset, "blockchain": blockchain, "network": network},
71 | )
72 | logger.debug(f"Response: {response.text}")
73 | validate_response(response)
74 | return Wallets.from_json(response.json())
75 | else:
76 | response = await self.http_client.get("getBalance")
77 | logger.debug(f"Response: {response.text}")
78 | validate_response(response)
79 | return Wallets.from_json(response.json())
80 |
81 | async def get_asset(self, asset: str, blockchain: str) -> Asset:
82 | """
83 | Get asset details
84 | """
85 | response = await self.http_client.request(
86 | method="GET",
87 | url="getAsset",
88 | json={
89 | "asset": asset,
90 | "blockchain": blockchain,
91 | },
92 | )
93 | logger.debug(f"Response: {response.text}")
94 | validate_response(response)
95 | return Asset.from_json(response.json())
96 |
97 | async def get_assets(self) -> Assets:
98 | """
99 | Get assets suported by TelePay
100 | """
101 | response = await self.http_client.get("getAssets")
102 | logger.debug(f"Response: {response.text}")
103 | validate_response(response)
104 | return Assets.from_json(response.json())
105 |
106 | async def get_invoices(self) -> InvoiceList:
107 | """
108 | Get your merchant invoices
109 | """
110 | response = await self.http_client.get("getInvoices")
111 | logger.debug(f"Response: {response.text}")
112 | validate_response(response)
113 | return InvoiceList.from_json(response.json())
114 |
115 | async def get_invoice(self, number: str) -> Invoice:
116 | """
117 | Get invoice details, by ID
118 | """
119 | response = await self.http_client.get(f"getInvoice/{number}")
120 | logger.debug(f"Response: {response.text}")
121 | validate_response(response)
122 | return Invoice.from_json(response.json())
123 |
124 | async def create_invoice(
125 | self,
126 | asset: str,
127 | blockchain: str,
128 | network: str,
129 | amount: float,
130 | success_url: str,
131 | cancel_url: str,
132 | expires_at: int,
133 | metadata: dict = None,
134 | description: str = None,
135 | ) -> Invoice:
136 | """
137 | Create an invoice
138 | """
139 | response = await self.http_client.post(
140 | "createInvoice",
141 | json={
142 | "asset": asset,
143 | "blockchain": blockchain,
144 | "network": network,
145 | "amount": amount,
146 | "description": description,
147 | "metadata": metadata,
148 | "success_url": success_url,
149 | "cancel_url": cancel_url,
150 | "expires_at": expires_at,
151 | },
152 | )
153 | logger.debug(f"Response: {response.text}")
154 | validate_response(response)
155 | return Invoice.from_json(response.json())
156 |
157 | async def cancel_invoice(self, number: str) -> Invoice:
158 | """
159 | Cancel an invoice
160 | """
161 | response = await self.http_client.post(f"cancelInvoice/{number}")
162 | logger.debug(f"Response: {response.text}")
163 | validate_response(response)
164 | return Invoice.from_json(response.json())
165 |
166 | async def delete_invoice(self, number: str) -> dict:
167 | """
168 | Delete an invoice
169 | """
170 | response = await self.http_client.post(f"deleteInvoice/{number}")
171 | logger.debug(f"Response: {response.text}")
172 | validate_response(response)
173 | return response.json()
174 |
175 | async def transfer(
176 | self,
177 | asset: str,
178 | blockchain: str,
179 | network: str,
180 | amount: float,
181 | username: str,
182 | message: str = None,
183 | ) -> dict:
184 | """
185 | Transfer funds between internal wallets.
186 | Off-chain operation.
187 | """
188 | response = await self.http_client.post(
189 | "transfer",
190 | json={
191 | "asset": asset,
192 | "blockchain": blockchain,
193 | "network": network,
194 | "amount": amount,
195 | "username": username,
196 | "message": message,
197 | },
198 | )
199 | logger.debug(f"Response: {response.text}")
200 | validate_response(response)
201 | return response.json()
202 |
203 | async def get_withdraw_minimum(
204 | self,
205 | asset: str,
206 | blockchain: str,
207 | network: str = None,
208 | ) -> dict:
209 | """
210 | Get minimum withdraw amount.
211 | """
212 | response = await self.http_client.post(
213 | "getWithdrawMinimum",
214 | json={
215 | "asset": asset,
216 | "blockchain": blockchain,
217 | "network": network,
218 | },
219 | )
220 | logger.debug(f"Response: {response.text}")
221 | validate_response(response)
222 | return response.json()
223 |
224 | async def get_withdraw_fee(
225 | self,
226 | asset: str,
227 | blockchain: str,
228 | network: str,
229 | amount: float,
230 | to_address: str,
231 | message: str = None,
232 | ) -> dict:
233 | """
234 | Get estimated withdraw fee, composed of blockchain fee and processing fee.
235 | """
236 | response = await self.http_client.post(
237 | "getWithdrawFee",
238 | json={
239 | "to_address": to_address,
240 | "asset": asset,
241 | "blockchain": blockchain,
242 | "network": network,
243 | "amount": amount,
244 | "message": message,
245 | },
246 | )
247 | logger.debug(f"Response: {response.text}")
248 | validate_response(response)
249 | return response.json()
250 |
251 | async def withdraw(
252 | self,
253 | to_address: str,
254 | asset: str,
255 | blockchain: str,
256 | network: str,
257 | amount: float,
258 | message: str,
259 | ) -> dict:
260 | """
261 | Withdraw funds from merchant wallet to external wallet.
262 | On-chain operation.
263 | """
264 | response = await self.http_client.post(
265 | "withdraw",
266 | json={
267 | "to_address": to_address,
268 | "asset": asset,
269 | "blockchain": blockchain,
270 | "network": network,
271 | "amount": amount,
272 | "message": message,
273 | },
274 | )
275 | logger.debug(f"Response: {response.text}")
276 | validate_response(response)
277 | return response.json()
278 |
279 | async def create_webhook(
280 | self, url: str, secret: str, events: list, active: bool
281 | ) -> Webhook:
282 | """
283 | Create a webhook
284 | """
285 | response = await self.http_client.post(
286 | "createWebhook",
287 | json={
288 | "url": url,
289 | "secret": secret,
290 | "events": events,
291 | "active": active,
292 | },
293 | )
294 | logger.debug(f"Response: {response.text}")
295 | validate_response(response)
296 | return Webhook.from_json(response.json())
297 |
298 | async def update_webhook(
299 | self, id: str, url: str, secret: str, events: list, active: bool
300 | ) -> Webhook:
301 | """
302 | Update a webhook
303 | """
304 | response = await self.http_client.post(
305 | f"updateWebhook/{id}",
306 | json={
307 | "url": url,
308 | "secret": secret,
309 | "events": events,
310 | "active": active,
311 | },
312 | )
313 | logger.debug(f"Response: {response.text}")
314 | validate_response(response)
315 | return Webhook.from_json(response.json())
316 |
317 | async def activate_webhook(self, id: str) -> Webhook:
318 | """
319 | Activate a webhook
320 | """
321 | response = await self.http_client.post(f"activateWebhook/{id}")
322 | validate_response(response)
323 | return Webhook.from_json(response.json())
324 |
325 | async def deactivate_webhook(self, id: str) -> Webhook:
326 | """
327 | Deactivate a webhook
328 | """
329 | response = await self.http_client.post(f"deactivateWebhook/{id}")
330 | validate_response(response)
331 | return Webhook.from_json(response.json())
332 |
333 | async def delete_webhook(self, id: str) -> dict:
334 | """
335 | Delete a webhook
336 | """
337 | response = await self.http_client.post(f"deleteWebhook/{id}")
338 | logger.debug(f"Response: {response.text}")
339 | validate_response(response)
340 | return response.json()
341 |
342 | async def get_webhook(self, id: str) -> Webhook:
343 | """
344 | Get webhook
345 | """
346 | response = await self.http_client.get(f"getWebhook/{id}")
347 | logger.debug(f"Response: {response.text}")
348 | validate_response(response)
349 | return Webhook.from_json(response.json())
350 |
351 | async def get_webhooks(self) -> Webhooks:
352 | """
353 | Get webhooks
354 | """
355 | response = await self.http_client.get("getWebhooks")
356 | logger.debug(f"Response: {response.text}")
357 | validate_response(response)
358 | return Webhooks.from_json(response.json())
359 |
--------------------------------------------------------------------------------
/telepay/v1/_sync/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/telepay/v1/_sync/__init__.py
--------------------------------------------------------------------------------
/telepay/v1/_sync/client.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass, field
3 | from typing import Union
4 |
5 | from httpx._config import Timeout
6 | from httpx._types import TimeoutTypes
7 |
8 | from ..auth import TelePayAuth
9 | from ..http_clients import SyncClient
10 | from ..models.account import Account
11 | from ..models.assets import Asset, Assets
12 | from ..models.invoice import Invoice, InvoiceList
13 | from ..models.wallets import Wallet, Wallets
14 | from ..models.webhooks import Webhook, Webhooks
15 | from ..utils import validate_response
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | @dataclass
21 | class TelePaySyncClient:
22 | """
23 | Creates a TelePay client.
24 | * API_SECRET: Your merchant private API key.
25 | Any requests without this authentication key will result in error 403.
26 | """
27 |
28 | timeout: TimeoutTypes = field(default=Timeout(60))
29 |
30 | def __init__(self, secret_api_key, timeout=Timeout(60)) -> None:
31 | self.base_url = "https://api.telepay.cash/rest/"
32 | self.timeout = timeout
33 | self.http_client = SyncClient(
34 | base_url=self.base_url,
35 | headers={"Authorization": secret_api_key},
36 | timeout=self.timeout,
37 | )
38 |
39 | def __enter__(self) -> "TelePaySyncClient":
40 | return self
41 |
42 | def __exit__(self, exc_type, exc_val, exc_tb) -> None:
43 | self.close()
44 |
45 | def close(self) -> None:
46 | self.http_client.aclose()
47 |
48 | @staticmethod
49 | def from_auth(auth: TelePayAuth, timeout=Timeout(60)) -> "TelePaySyncClient":
50 | return TelePaySyncClient(auth.secret_api_key, timeout=timeout)
51 |
52 | def get_me(self) -> Account:
53 | """
54 | Info about the current account
55 | """
56 | response = self.http_client.get("getMe")
57 | logger.debug(f"response: {response.text}")
58 | validate_response(response)
59 | return Account.from_json(response.json())
60 |
61 | def get_balance(
62 | self, asset=None, blockchain=None, network=None
63 | ) -> Union[Wallet, Wallets]:
64 | """
65 | Get your merchant wallet assets with corresponding balance
66 | """
67 | if asset and blockchain and network:
68 | response = self.http_client.post(
69 | "getBalance",
70 | json={"asset": asset, "blockchain": blockchain, "network": network},
71 | )
72 | logger.debug(f"response: {response.text}")
73 | validate_response(response)
74 | return Wallets.from_json(response.json())
75 | else:
76 | response = self.http_client.get("getBalance")
77 | logger.debug(f"response: {response.text}")
78 | validate_response(response)
79 | return Wallets.from_json(response.json())
80 |
81 | def get_asset(self, asset: str, blockchain: str) -> Asset:
82 | """
83 | Get asset details
84 | """
85 | response = self.http_client.request(
86 | method="GET",
87 | url="getAsset",
88 | json={
89 | "asset": asset,
90 | "blockchain": blockchain,
91 | },
92 | )
93 | logger.debug(f"response: {response.text}")
94 | validate_response(response)
95 | return Asset.from_json(response.json())
96 |
97 | def get_assets(self) -> Assets:
98 | """
99 | Get assets suported by TelePay
100 | """
101 | response = self.http_client.get("getAssets")
102 | logger.debug(f"response: {response.text}")
103 | validate_response(response)
104 | return Assets.from_json(response.json())
105 |
106 | def get_invoices(self) -> InvoiceList:
107 | """
108 | Get your merchant invoices
109 | """
110 | response = self.http_client.get("getInvoices")
111 | logger.debug(f"response: {response.text}")
112 | validate_response(response)
113 | return InvoiceList.from_json(response.json())
114 |
115 | def get_invoice(self, number: str) -> Invoice:
116 | """
117 | Get invoice details, by ID
118 | """
119 | response = self.http_client.get(f"getInvoice/{number}")
120 | logger.debug(f"response: {response.text}")
121 | validate_response(response)
122 | return Invoice.from_json(response.json())
123 |
124 | def create_invoice(
125 | self,
126 | asset: str,
127 | blockchain: str,
128 | network: str,
129 | amount: float,
130 | success_url: str,
131 | cancel_url: str,
132 | expires_at: int,
133 | metadata: dict = None,
134 | description: str = None,
135 | ) -> Invoice:
136 | """
137 | Create an invoice
138 | """
139 | response = self.http_client.post(
140 | "createInvoice",
141 | json={
142 | "asset": asset,
143 | "blockchain": blockchain,
144 | "network": network,
145 | "amount": amount,
146 | "description": description,
147 | "metadata": metadata,
148 | "success_url": success_url,
149 | "cancel_url": cancel_url,
150 | "expires_at": expires_at,
151 | },
152 | )
153 | logger.debug(f"response: {response.text}")
154 | validate_response(response)
155 | return Invoice.from_json(response.json())
156 |
157 | def cancel_invoice(self, number: str) -> Invoice:
158 | """
159 | Cancel an invoice
160 | """
161 | response = self.http_client.post(f"cancelInvoice/{number}")
162 | logger.debug(f"response: {response.text}")
163 | validate_response(response)
164 | return Invoice.from_json(response.json())
165 |
166 | def delete_invoice(self, number: str) -> dict:
167 | """
168 | Delete an invoice
169 | """
170 | response = self.http_client.post(f"deleteInvoice/{number}")
171 | logger.debug(f"response: {response.text}")
172 | validate_response(response)
173 | return response.json()
174 |
175 | def transfer(
176 | self,
177 | asset: str,
178 | blockchain: str,
179 | network: str,
180 | amount: float,
181 | username: str,
182 | message: str = None,
183 | ) -> dict:
184 | """
185 | Transfer funds between internal wallets.
186 | Off-chain operation.
187 | """
188 | response = self.http_client.post(
189 | "transfer",
190 | json={
191 | "asset": asset,
192 | "blockchain": blockchain,
193 | "network": network,
194 | "amount": amount,
195 | "username": username,
196 | "message": message,
197 | },
198 | )
199 | logger.debug(f"response: {response.text}")
200 | validate_response(response)
201 | return response.json()
202 |
203 | def get_withdraw_minimum(
204 | self,
205 | asset: str,
206 | blockchain: str,
207 | network: str = None,
208 | ) -> dict:
209 | """
210 | Get minimum withdraw amount.
211 | """
212 | response = self.http_client.post(
213 | "getWithdrawMinimum",
214 | json={
215 | "asset": asset,
216 | "blockchain": blockchain,
217 | "network": network,
218 | },
219 | )
220 | logger.debug(f"response: {response.text}")
221 | validate_response(response)
222 | return response.json()
223 |
224 | def get_withdraw_fee(
225 | self,
226 | to_address: str,
227 | asset: str,
228 | blockchain: str,
229 | network: str,
230 | amount: float,
231 | message: str = None,
232 | ) -> dict:
233 | """
234 | Get estimated withdraw fee, composed of blockchain fee and processing fee.
235 | """
236 | response = self.http_client.post(
237 | "getWithdrawFee",
238 | json={
239 | "to_address": to_address,
240 | "asset": asset,
241 | "blockchain": blockchain,
242 | "network": network,
243 | "amount": amount,
244 | "message": message,
245 | },
246 | )
247 | logger.debug(f"response: {response.text}")
248 | validate_response(response)
249 | return response.json()
250 |
251 | def withdraw(
252 | self,
253 | to_address: str,
254 | asset: str,
255 | blockchain: str,
256 | network: str,
257 | amount: float,
258 | message: str,
259 | ) -> dict:
260 | """
261 | Withdraw funds from merchant wallet to external wallet.
262 | On-chain operation.
263 | """
264 | response = self.http_client.post(
265 | "withdraw",
266 | json={
267 | "to_address": to_address,
268 | "asset": asset,
269 | "blockchain": blockchain,
270 | "network": network,
271 | "amount": amount,
272 | "message": message,
273 | },
274 | )
275 | logger.debug(f"response: {response.text}")
276 | validate_response(response)
277 | return response.json()
278 |
279 | def create_webhook(
280 | self, url: str, secret: str, events: list, active: bool
281 | ) -> Webhook:
282 | """
283 | Create a webhook
284 | """
285 | response = self.http_client.post(
286 | "createWebhook",
287 | json={
288 | "url": url,
289 | "secret": secret,
290 | "events": events,
291 | "active": active,
292 | },
293 | )
294 | logger.debug(f"response: {response.text}")
295 | validate_response(response)
296 | return Webhook.from_json(response.json())
297 |
298 | def update_webhook(
299 | self, id: str, url: str, secret: str, events: list, active: bool
300 | ) -> Webhook:
301 | """
302 | Update a webhook
303 | """
304 | response = self.http_client.post(
305 | f"updateWebhook/{id}",
306 | json={
307 | "url": url,
308 | "secret": secret,
309 | "events": events,
310 | "active": active,
311 | },
312 | )
313 | logger.debug(f"response: {response.text}")
314 | validate_response(response)
315 | return Webhook.from_json(response.json())
316 |
317 | def activate_webhook(self, id: str) -> Webhook:
318 | """
319 | Activate a webhook
320 | """
321 | response = self.http_client.post(f"activateWebhook/{id}")
322 | logger.debug(f"response: {response.text}")
323 | validate_response(response)
324 | return Webhook.from_json(response.json())
325 |
326 | def deactivate_webhook(self, id: str) -> Webhook:
327 | """
328 | Deactivate a webhook
329 | """
330 | response = self.http_client.post(f"deactivateWebhook/{id}")
331 | logger.debug(f"response: {response.text}")
332 | validate_response(response)
333 | return Webhook.from_json(response.json())
334 |
335 | def delete_webhook(self, id: str) -> dict:
336 | """
337 | Delete a webhook
338 | """
339 | response = self.http_client.post(f"deleteWebhook/{id}")
340 | logger.debug(f"response: {response.text}")
341 | validate_response(response)
342 | return response.json()
343 |
344 | def get_webhook(self, id: str) -> Webhook:
345 | """
346 | Get webhook
347 | """
348 | response = self.http_client.get(f"getWebhook/{id}")
349 | logger.debug(f"response: {response.text}")
350 | validate_response(response)
351 | return Webhook.from_json(response.json())
352 |
353 | def get_webhooks(self) -> Webhooks:
354 | """
355 | Get webhooks
356 | """
357 | response = self.http_client.get("getWebhooks")
358 | logger.debug(f"response: {response.text}")
359 | validate_response(response)
360 | return Webhooks.from_json(response.json())
361 |
--------------------------------------------------------------------------------
/telepay/v1/auth.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from os import environ
3 |
4 | from dotenv import load_dotenv
5 |
6 | from .errors import TelePayError
7 |
8 | load_dotenv()
9 |
10 |
11 | @dataclass
12 | class TelePayAuth:
13 | secret_api_key: str = field(default=environ["TELEPAY_SECRET_API_KEY"])
14 |
15 | def __init__(self, secret_api_key=None):
16 | if secret_api_key:
17 | self.secret_api_key = secret_api_key
18 | else:
19 | self.secret_api_key = environ["TELEPAY_SECRET_API_KEY"]
20 | self.__post_init__()
21 |
22 | def __post_init__(self):
23 | if not self.secret_api_key:
24 | raise TelePayError(0, "TELEPAY_SECRET_API_KEY is not setted")
25 |
--------------------------------------------------------------------------------
/telepay/v1/errors.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from typing import Optional
3 |
4 |
5 | @dataclass
6 | class TelePayError(Exception):
7 | status_code: int
8 | error: str
9 | message: Optional[str] = field(default=None)
10 |
--------------------------------------------------------------------------------
/telepay/v1/http_clients.py:
--------------------------------------------------------------------------------
1 | from httpx import AsyncClient # noqa
2 | from httpx import Client as BaseClient
3 |
4 |
5 | class SyncClient(BaseClient):
6 | def aclose(self) -> None:
7 | self.close()
8 |
--------------------------------------------------------------------------------
/telepay/v1/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/telepay/v1/models/__init__.py
--------------------------------------------------------------------------------
/telepay/v1/models/account.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 | from typing import Any
4 |
5 | from ..utils import parse_json
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @dataclass
11 | class Account:
12 | merchant: dict
13 |
14 | def __post_init__(self):
15 | pass
16 |
17 | @classmethod
18 | def from_json(cls, json: Any) -> "Account":
19 | del json["version"]
20 | logger.debug(f"Parsing Account from JSON: {json}")
21 | return parse_json(cls, **json)
22 |
--------------------------------------------------------------------------------
/telepay/v1/models/assets.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 | from typing import Any, List
4 |
5 | from ..utils import parse_json
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @dataclass
11 | class Asset:
12 | asset: str
13 | blockchain: str
14 | usd_price: float
15 | url: str
16 | networks: List[str]
17 | coingecko_id: str
18 |
19 | @classmethod
20 | def from_json(cls, json: Any) -> "Asset":
21 | return parse_json(cls, **json)
22 |
23 |
24 | @dataclass
25 | class Assets:
26 | assets: List[Asset]
27 |
28 | def __post_init__(self):
29 | pass
30 |
31 | @classmethod
32 | def from_json(cls, json: Any) -> "Assets":
33 | logger.debug(f"Parsing Assets from JSON: {json}")
34 | return parse_json(cls, **json)
35 |
--------------------------------------------------------------------------------
/telepay/v1/models/invoice.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 | from datetime import datetime
4 | from typing import Any
5 |
6 | from ..utils import parse_json
7 |
8 | FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | @dataclass
14 | class Invoice:
15 | asset: str
16 | blockchain: str
17 | network: str
18 | amount: str
19 | description: str
20 |
21 | number: str
22 | status: str
23 | metadata: str
24 |
25 | success_url: str
26 | cancel_url: str
27 |
28 | created_at: datetime
29 | updated_at: datetime
30 | expires_at: datetime
31 |
32 | # after created
33 | checkout_url: str
34 | onchain_url: str
35 |
36 | # after payment completed
37 | explorer_url: str
38 |
39 | def __post_init__(self):
40 | self.asset = str(self.asset)
41 | self.blockchain = str(self.blockchain)
42 | self.network = str(self.network)
43 | self.amount = str(self.amount)
44 | self.description = str(self.description)
45 | self.success_url = str(self.success_url)
46 | self.cancel_url = str(self.cancel_url)
47 | self.status = str(self.status)
48 | self.number = str(self.number)
49 | self.metadata = str(self.metadata)
50 | self.created_at = datetime.strptime(self.created_at, FORMAT)
51 | self.expires_at = datetime.strptime(self.expires_at, FORMAT)
52 |
53 | if self.updated_at is not None:
54 | self.updated_at = datetime.strptime(self.updated_at, FORMAT)
55 |
56 | @classmethod
57 | def from_json(cls, json: Any) -> "Invoice":
58 | logger.debug(f"Parsing Invoice from JSON: {json}")
59 | return parse_json(cls, **json)
60 |
61 |
62 | @dataclass
63 | class InvoiceList:
64 | invoices: list
65 |
66 | def __post_init__(self):
67 | self.invoices = [Invoice.from_json(invoice) for invoice in self.invoices]
68 |
69 | @classmethod
70 | def from_json(cls, json: Any) -> "InvoiceList":
71 | logger.debug(f"Parsing InvoiceList from JSON: {json}")
72 | return parse_json(cls, **json)
73 |
--------------------------------------------------------------------------------
/telepay/v1/models/wallets.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 | from typing import Any, List
4 |
5 | from ..utils import parse_json
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @dataclass
11 | class Wallet:
12 | asset: str
13 | blockchain: str
14 | network: str
15 | balance: float
16 |
17 | @classmethod
18 | def from_json(cls, json: Any) -> "Wallet":
19 | logger.debug(f"Parsing wallet from JSON: {json}")
20 | return parse_json(cls, **json)
21 |
22 |
23 | @dataclass
24 | class Wallets:
25 | wallets: List[Wallet]
26 |
27 | def __post_init__(self):
28 | pass
29 |
30 | @classmethod
31 | def from_json(cls, json: Any) -> "Wallets":
32 | logger.debug(f"Parsing wallets from JSON: {json}")
33 | return parse_json(cls, **json)
34 |
--------------------------------------------------------------------------------
/telepay/v1/models/webhooks.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 | from typing import Any, List
4 |
5 | from ..utils import parse_json
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @dataclass
11 | class Webhook:
12 | id: str
13 | url: str
14 | secret: str
15 | events: List[str]
16 | active: bool
17 |
18 | @classmethod
19 | def from_json(cls, json: Any) -> "Webhook":
20 | logger.debug(f"Parsing Webhook from JSON: {json}")
21 | return parse_json(cls, **json)
22 |
23 |
24 | @dataclass
25 | class Webhooks:
26 | webhooks: List[Webhook]
27 |
28 | def __post_init__(self):
29 | self.webhooks = [Webhook.from_json(webhook) for webhook in self.webhooks]
30 |
31 | @classmethod
32 | def from_json(cls, json: Any) -> "Webhooks":
33 | logger.debug(f"Parsing Webhooks from JSON: {json}")
34 | return parse_json(cls, **json)
35 |
--------------------------------------------------------------------------------
/telepay/v1/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from inspect import signature
3 | from json import JSONDecodeError
4 | from typing import Any, Type, TypeVar
5 |
6 | from httpx import Response
7 |
8 | from .errors import TelePayError
9 |
10 | T = TypeVar("T")
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | def validate_response(response: Response) -> None:
16 | if response.status_code < 200 or response.status_code >= 300:
17 | error_data = {}
18 | try:
19 | error_data = response.json()
20 | except JSONDecodeError as e:
21 | logger.error(e.msg)
22 | logger.error(f"{response.content} couldn't parse to json")
23 | finally:
24 | error = error_data.pop("error", response.status_code)
25 | message = error_data.pop("message", response.content)
26 | raise TelePayError(
27 | status_code=response.status_code,
28 | error=error,
29 | message=message,
30 | )
31 |
32 |
33 | def parse_json(cls: Type[T], **json: Any) -> T:
34 | cls_fields = {field for field in signature(cls).parameters}
35 | native_args, new_args = {}, {}
36 | for name, val in json.items():
37 | if name in cls_fields:
38 | native_args[name] = val
39 | else:
40 | new_args[name] = val
41 | ret = cls(**native_args)
42 | for new_name, new_val in new_args.items():
43 | setattr(ret, new_name, new_val)
44 | return ret
45 |
--------------------------------------------------------------------------------
/telepay/v1/webhooks.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import logging
4 | from dataclasses import dataclass
5 |
6 | import uvicorn
7 | from colorama import Fore, Style
8 | from fastapi import FastAPI, Request
9 |
10 | from .errors import TelePayError
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 | # events
15 | INVOICE_COMPLETED = "invoice.completed"
16 | INVOICE_CANCELLED = "invoice.cancelled"
17 | INVOICE_EXPIRED = "invoice.expired"
18 | INVOICE_DELETED = "invoice.deleted"
19 |
20 |
21 | def get_signature(data, secret):
22 | """
23 | Get the webhook signature using the request data and your secret API key
24 | """
25 | hash_secret = hashlib.sha1(secret.encode()).hexdigest()
26 | hash_data = hashlib.sha512(data.encode()).hexdigest()
27 | signature = hashlib.sha512((hash_secret + hash_data).encode()).hexdigest()
28 | return signature
29 |
30 |
31 | @dataclass
32 | class TelePayWebhookListener:
33 | secret: str
34 | callback: callable
35 | host: str = "localhost"
36 | port: str = 5000
37 | url: str = "/webhook"
38 | log_level: str = "error"
39 |
40 | app = FastAPI()
41 |
42 | def __post_init__(self):
43 | @self.app.post(self.url)
44 | async def listen_webhook(request: Request):
45 | data = str(json.loads(await request.json()))
46 |
47 | request_signature = request.headers["Webhook-Signature"]
48 |
49 | signature = get_signature(str(data), self.secret)
50 |
51 | if signature != request_signature:
52 | logger.debug(f"Signature mismatch: {signature} != {request_signature}")
53 |
54 | raise TelePayError(
55 | message="Invalid signature",
56 | status_code=400,
57 | error="invalid_signature",
58 | )
59 |
60 | self.callback(request.headers, data)
61 |
62 | return "Thanks TelePay"
63 |
64 | def listen(self):
65 | url = f"http://{self.host}:{self.port}{self.url}"
66 | logger.debug(f"Listening on {url}")
67 |
68 | print(
69 | Fore.CYAN
70 | + r"""
71 | _____ _ ______
72 | |_ _| | | | ___ \
73 | | | ___| | ___| |_/ /_ _ _ _
74 | | |/ _ \ |/ _ \ __/ _` | | | |
75 | | | __/ | __/ | | (_| | |_| |
76 | \_/\___|_|\___\_| \__,_|\__, |.cash ⚡️
77 | __/ |
78 | |___/
79 | """
80 | )
81 | print(
82 | Style.RESET_ALL
83 | + f"""
84 | [ Webhook listener ]
85 |
86 | * Docs: https://telepay.readme.io/reference/webhooks
87 | * Listening on {url} (Press CTRL+C to quit)
88 | """
89 | )
90 | uvicorn.run(self.app, host=self.host, port=self.port, log_level=self.log_level)
91 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/tests/__init__.py
--------------------------------------------------------------------------------
/tests/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/tests/v1/__init__.py
--------------------------------------------------------------------------------
/tests/v1/_async/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/tests/v1/_async/__init__.py
--------------------------------------------------------------------------------
/tests/v1/_async/test_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 |
4 | from httpx import Timeout
5 | from pytest import fixture
6 | from pytest import mark as pytest_mark
7 |
8 | from telepay.v1 import Invoice, TelePayAsyncClient, TelePayAuth, TelePayError, Webhook
9 |
10 | from ..utils import ERRORS, random_text
11 |
12 | TIMEOUT = 60
13 |
14 |
15 | @fixture(name="client")
16 | async def create_client():
17 | client = TelePayAsyncClient.from_auth(TelePayAuth(), timeout=Timeout(TIMEOUT))
18 | yield client
19 | await client.close()
20 |
21 |
22 | @fixture(name="invoice")
23 | async def create_invoice(client: TelePayAsyncClient):
24 | invoice = await client.create_invoice(
25 | asset="TON",
26 | blockchain="TON",
27 | network="testnet",
28 | amount=1,
29 | description="Testing",
30 | metadata={"color": "red", "size": "large"},
31 | success_url="https://example.com/success",
32 | cancel_url="https://example.com/cancel",
33 | expires_at=1,
34 | )
35 | yield invoice
36 |
37 |
38 | @fixture(name="webhook")
39 | async def create_webhook(client: TelePayAsyncClient):
40 | webhook = await client.create_webhook(
41 | url=f"https://{uuid.uuid4().hex}.com",
42 | secret="hello",
43 | events=["all"],
44 | active=False,
45 | )
46 | yield webhook
47 |
48 |
49 | @pytest_mark.anyio
50 | async def test_error(client: TelePayAsyncClient):
51 | client = TelePayAsyncClient("")
52 | try:
53 | await client.get_me()
54 | assert False
55 | except TelePayError:
56 | assert True
57 |
58 |
59 | @pytest_mark.anyio
60 | async def test_client_with_context():
61 | api_key = os.environ["TELEPAY_SECRET_API_KEY"]
62 | async with TelePayAsyncClient(secret_api_key=api_key) as client:
63 | assert client is not None
64 |
65 |
66 | @pytest_mark.anyio
67 | async def test_get_me(client: TelePayAsyncClient):
68 | try:
69 | await client.get_me()
70 | except TelePayError as e:
71 | if e.status_code == 403:
72 | assert e.error == "forbidden"
73 | assert e.message == ERRORS["forbidden"]
74 |
75 |
76 | @pytest_mark.anyio
77 | async def test_get_balance(client: TelePayAsyncClient):
78 | try:
79 | await client.get_balance()
80 | await client.get_balance(asset="TON", blockchain="TON", network="testnet")
81 | except TelePayError as e:
82 | if e.status_code == 403:
83 | assert e.error == "forbidden"
84 | assert e.message == ERRORS["forbidden"]
85 |
86 |
87 | @pytest_mark.anyio
88 | async def test_get_asset(client: TelePayAsyncClient):
89 | try:
90 | await client.get_asset(asset="TON", blockchain="TON")
91 | except TelePayError as e:
92 | if e.status_code == 403:
93 | assert e.error == "forbidden"
94 | assert e.message == ERRORS["forbidden"]
95 |
96 |
97 | @pytest_mark.anyio
98 | async def test_get_assets(client: TelePayAsyncClient):
99 | try:
100 | await client.get_assets()
101 | except TelePayError as e:
102 | if e.status_code == 403:
103 | assert e.error == "forbidden"
104 | assert e.message == ERRORS["forbidden"]
105 |
106 |
107 | @pytest_mark.anyio
108 | async def test_get_invoices(client: TelePayAsyncClient):
109 | try:
110 | await client.get_invoices()
111 | except TelePayError as e:
112 | if e.status_code == 403:
113 | assert e.error == "forbidden"
114 | assert e.message == ERRORS["forbidden"]
115 |
116 |
117 | @pytest_mark.anyio
118 | async def test_get_invoice(client: TelePayAsyncClient, invoice: Invoice):
119 | try:
120 | await client.get_invoice(invoice.number)
121 | except TelePayError as e:
122 | if e.status_code == 403:
123 | assert e.error == "forbidden"
124 | assert e.message == ERRORS["forbidden"]
125 |
126 |
127 | @pytest_mark.anyio
128 | async def test_get_invoice_not_found(client: TelePayAsyncClient):
129 | number = random_text(10)
130 | try:
131 | await client.get_invoice(number)
132 | assert False
133 | except TelePayError as e:
134 | if e.status_code == 403:
135 | assert e.error == "forbidden"
136 | assert e.message == ERRORS["forbidden"]
137 | elif e.status_code == 404:
138 | assert e.error == "invoice.not-found"
139 | assert e.message == ERRORS["invoice.not-found"]
140 |
141 |
142 | @pytest_mark.anyio
143 | async def test_cancel_invoice(client: TelePayAsyncClient, invoice: Invoice):
144 | try:
145 | await client.cancel_invoice(invoice.number)
146 | except TelePayError as e:
147 | if e.status_code == 403:
148 | assert e.error == "forbidden"
149 | assert e.message == ERRORS["forbidden"]
150 | elif e.status_code == 404:
151 | assert e.error == "invoice.not-found"
152 | assert e.message == ERRORS["invoice.not-found"]
153 |
154 |
155 | @pytest_mark.anyio
156 | async def test_cancel_invoice_not_found(client: TelePayAsyncClient):
157 | number = random_text(10)
158 | try:
159 | await client.cancel_invoice(number)
160 | except TelePayError as e:
161 | if e.status_code == 403:
162 | assert e.error == "forbidden"
163 | assert e.message == ERRORS["forbidden"]
164 | elif e.status_code == 404:
165 | assert e.error == "invoice.not-found"
166 | assert e.message == ERRORS["invoice.not-found"]
167 |
168 |
169 | @pytest_mark.anyio
170 | async def test_delete_invoice(client: TelePayAsyncClient, invoice: Invoice):
171 | try:
172 | await client.cancel_invoice(invoice.number)
173 | await client.delete_invoice(invoice.number)
174 | except TelePayError as e:
175 | if e.status_code == 403:
176 | assert e.error == "forbidden"
177 | assert e.message == ERRORS["forbidden"]
178 | elif e.status_code == 404:
179 | assert e.error == "invoice.not-found"
180 | assert e.message == ERRORS["invoice.not-found"]
181 |
182 |
183 | @pytest_mark.anyio
184 | async def test_delete_invoice_not_found(client: TelePayAsyncClient):
185 | number = random_text(10)
186 | try:
187 | await client.delete_invoice(number)
188 | assert False
189 | except TelePayError as e:
190 | if e.status_code == 403:
191 | assert e.error == "forbidden"
192 | assert e.message == ERRORS["forbidden"]
193 | elif e.status_code == 404:
194 | assert e.error == "invoice.not-found"
195 | assert e.message == ERRORS["invoice.not-found"]
196 |
197 |
198 | @pytest_mark.anyio
199 | async def test_transfer_without_funds(client: TelePayAsyncClient):
200 | try:
201 | await client.transfer(
202 | asset="TON",
203 | blockchain="TON",
204 | network="testnet",
205 | amount=1,
206 | username="telepay",
207 | )
208 | except TelePayError as e:
209 | if e.status_code == 401:
210 | assert e.error == "transfer.insufficient-funds"
211 | assert e.message == ERRORS["transfer.insufficient-funds"]
212 | elif e.status_code == 403:
213 | assert e.error == "forbidden"
214 | assert e.message == ERRORS["forbidden"]
215 | elif e.status_code == 404:
216 | assert e.error == "invoice.not-found"
217 | assert e.message == ERRORS["invoice.not-found"]
218 |
219 |
220 | @pytest_mark.anyio
221 | async def test_transfer_to_wrong_user(client: TelePayAsyncClient):
222 | username = random_text(20)
223 | try:
224 | await client.transfer(
225 | asset="TON",
226 | blockchain="TON",
227 | network="testnet",
228 | amount=1,
229 | username=username,
230 | )
231 | except TelePayError as e:
232 | if e.status_code == 401:
233 | assert e.error == "transfer.insufficient-funds"
234 | assert e.message == ERRORS["transfer.insufficient-funds"]
235 | elif e.status_code == 403:
236 | assert e.error == "forbidden"
237 | assert e.message == ERRORS["forbidden"]
238 | elif e.status_code == 404:
239 | assert e.error == "account.not-found"
240 | assert e.message == ERRORS["account.not-found"]
241 |
242 |
243 | @pytest_mark.anyio
244 | async def test_transfer_to_itself(client: TelePayAsyncClient):
245 | account = await client.get_me()
246 | username = account.merchant["username"]
247 | try:
248 | await client.transfer(
249 | asset="TON",
250 | blockchain="TON",
251 | network="testnet",
252 | amount=1,
253 | username=username,
254 | )
255 | except TelePayError as e:
256 | if e.status_code == 401:
257 | assert e.error == "transfer.not-possible"
258 | assert e.message == ERRORS["transfer.not-possible"]
259 | elif e.status_code == 403:
260 | assert e.error == "forbidden"
261 | assert e.message == ERRORS["forbidden"]
262 |
263 |
264 | @pytest_mark.anyio
265 | async def test_withdraw_minimum(client: TelePayAsyncClient):
266 | try:
267 | await client.get_withdraw_minimum(
268 | asset="TON",
269 | blockchain="TON",
270 | network="testnet",
271 | )
272 | except TelePayError as e:
273 | if e.status_code == 403:
274 | assert e.error == "forbidden"
275 | assert e.message == ERRORS["forbidden"]
276 |
277 |
278 | @pytest_mark.anyio
279 | async def test_get_withdraw_fee(client: TelePayAsyncClient):
280 | try:
281 | await client.get_withdraw_fee(
282 | to_address="EQCKYK7bYBt1t8UmdhImrbiSzC5ijfo_H3Zc_Hk8ksRpOkOk",
283 | asset="TON",
284 | blockchain="TON",
285 | network="testnet",
286 | amount=1,
287 | message="test",
288 | )
289 | except TelePayError as e:
290 | if e.status_code == 403:
291 | assert e.error == "forbidden"
292 | assert e.message == ERRORS["forbidden"]
293 |
294 |
295 | @pytest_mark.anyio
296 | async def test_withdraw(client: TelePayAsyncClient):
297 | try:
298 | await client.withdraw(
299 | to_address="EQCKYK7bYBt1t8UmdhImrbiSzC5ijfo_H3Zc_Hk8ksRpOkOk",
300 | asset="TON",
301 | blockchain="TON",
302 | network="testnet",
303 | amount=1,
304 | message="test",
305 | )
306 | except TelePayError as e:
307 | if e.status_code == 503:
308 | assert e.error == "unavailable"
309 | assert e.message == ERRORS["unavailable"]
310 | elif e.status_code == 401:
311 | assert e.error == "withdrawal.insufficient-funds"
312 | assert e.message == ERRORS["withdrawal.insufficient-funds"]
313 | elif e.status_code == 403:
314 | assert e.error == "forbidden"
315 | assert e.message == ERRORS["forbidden"]
316 |
317 |
318 | @pytest_mark.anyio
319 | async def test_update_webhook(client: TelePayAsyncClient, webhook: Webhook):
320 | try:
321 | await client.update_webhook(
322 | id=webhook.id,
323 | url="https://example.com",
324 | secret="hello",
325 | events=["invoice.completed"],
326 | active=False,
327 | )
328 | await client.delete_webhook(id=webhook.id)
329 | except TelePayError as e:
330 | if e.status_code == 403:
331 | assert e.error == "forbidden"
332 | assert e.message == ERRORS["forbidden"]
333 | if e.status_code == 404:
334 | assert e.error == "webhook.not-found"
335 | assert e.message == ERRORS["webhook.not-found"]
336 |
337 |
338 | @pytest_mark.anyio
339 | async def test_activate_webhook(client: TelePayAsyncClient, webhook: Webhook):
340 | try:
341 | await client.activate_webhook(id=webhook.id)
342 | await client.delete_webhook(id=webhook.id)
343 | except TelePayError as e:
344 | if e.status_code == 403:
345 | assert e.error == "forbidden"
346 | assert e.message == ERRORS["forbidden"]
347 | if e.status_code == 404:
348 | assert e.error == "webhook.not-found"
349 | assert e.message == ERRORS["webhook.not-found"]
350 |
351 |
352 | @pytest_mark.anyio
353 | async def test_deactivate_webhook(client: TelePayAsyncClient, webhook: Webhook):
354 | try:
355 | await client.deactivate_webhook(id=webhook.id)
356 | await client.delete_webhook(id=webhook.id)
357 | except TelePayError as e:
358 | if e.status_code == 403:
359 | assert e.error == "forbidden"
360 | assert e.message == ERRORS["forbidden"]
361 | if e.status_code == 404:
362 | assert e.error == "webhook.not-found"
363 | assert e.message == ERRORS["webhook.not-found"]
364 |
365 |
366 | @pytest_mark.anyio
367 | async def test_delete_webhook(client: TelePayAsyncClient, webhook: Webhook):
368 | try:
369 | await client.delete_webhook(id=webhook.id)
370 | except TelePayError as e:
371 | if e.status_code == 403:
372 | assert e.error == "forbidden"
373 | assert e.message == ERRORS["forbidden"]
374 | if e.status_code == 404:
375 | assert e.error == "webhook.not-found"
376 | assert e.message == ERRORS["webhook.not-found"]
377 |
378 |
379 | @pytest_mark.anyio
380 | async def test_get_webhook(client: TelePayAsyncClient, webhook: Webhook):
381 | try:
382 | await client.get_webhook(id=webhook.id)
383 | await client.delete_webhook(id=webhook.id)
384 | except TelePayError as e:
385 | if e.status_code == 403:
386 | assert e.error == "forbidden"
387 | assert e.message == ERRORS["forbidden"]
388 | if e.status_code == 404:
389 | assert e.error == "webhook.not-found"
390 | assert e.message == ERRORS["webhook.not-found"]
391 |
392 |
393 | @pytest_mark.anyio
394 | async def test_get_webhooks(client: TelePayAsyncClient):
395 | try:
396 | await client.get_webhooks()
397 | except TelePayError as e:
398 | if e.status_code == 403:
399 | assert e.error == "forbidden"
400 | assert e.message == ERRORS["forbidden"]
401 | if e.status_code == 404:
402 | assert e.error == "webhook.not-found"
403 | assert e.message == ERRORS["webhook.not-found"]
404 |
--------------------------------------------------------------------------------
/tests/v1/_sync/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelePay-cash/telepay-python/50d0cee6945bb3b389fa1745f135c992c681a7d8/tests/v1/_sync/__init__.py
--------------------------------------------------------------------------------
/tests/v1/_sync/test_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 |
4 | import pytest
5 | from httpx import Timeout
6 | from pytest import fixture
7 |
8 | from telepay.v1 import Invoice, TelePayAuth, TelePayError, TelePaySyncClient, Webhook
9 |
10 | from ..utils import ERRORS, random_text
11 |
12 | TIMEOUT = 60
13 |
14 |
15 | @fixture(name="client")
16 | def create_client():
17 | client = TelePaySyncClient.from_auth(TelePayAuth(), timeout=Timeout(TIMEOUT))
18 | yield client
19 | client.close()
20 |
21 |
22 | @fixture(name="invoice")
23 | def create_invoice(client: TelePaySyncClient):
24 | invoice = client.create_invoice(
25 | asset="TON",
26 | blockchain="TON",
27 | network="testnet",
28 | amount=1,
29 | description="Testing",
30 | metadata={"color": "red", "size": "large"},
31 | success_url="https://example.com/success",
32 | cancel_url="https://example.com/cancel",
33 | expires_at=1,
34 | )
35 | yield invoice
36 |
37 |
38 | @fixture(name="webhook")
39 | def create_webhook(client: TelePaySyncClient):
40 | webhook = client.create_webhook(
41 | url=f"https://{uuid.uuid4().hex}.com",
42 | secret="hello",
43 | events=["all"],
44 | active=False,
45 | )
46 | yield webhook
47 |
48 |
49 | def test_error(client: TelePaySyncClient):
50 | client = TelePaySyncClient("")
51 | with pytest.raises(TelePayError):
52 | client.get_me()
53 |
54 |
55 | def test_client_with_context():
56 | api_key = os.environ["TELEPAY_SECRET_API_KEY"]
57 | with TelePaySyncClient(secret_api_key=api_key) as client:
58 | assert client is not None
59 |
60 |
61 | def test_get_me(client: TelePaySyncClient):
62 | try:
63 | client.get_me()
64 | except TelePayError as e:
65 | if e.status_code == 403:
66 | assert e.error == "forbidden"
67 | assert e.message == ERRORS["forbidden"]
68 |
69 |
70 | def test_get_balance(client: TelePaySyncClient):
71 | try:
72 | client.get_balance()
73 | client.get_balance(asset="TON", blockchain="TON", network="testnet")
74 | except TelePayError as e:
75 | if e.status_code == 403:
76 | assert e.error == "forbidden"
77 | assert e.message == ERRORS["forbidden"]
78 |
79 |
80 | def test_get_asset(client: TelePaySyncClient):
81 | try:
82 | client.get_asset(asset="TON", blockchain="TON")
83 | except TelePayError as e:
84 | if e.status_code == 403:
85 | assert e.error == "forbidden"
86 | assert e.message == ERRORS["forbidden"]
87 |
88 |
89 | def test_get_assets(client: TelePaySyncClient):
90 | try:
91 | client.get_assets()
92 | except TelePayError as e:
93 | if e.status_code == 403:
94 | assert e.error == "forbidden"
95 | assert e.message == ERRORS["forbidden"]
96 |
97 |
98 | def test_get_invoices(client: TelePaySyncClient):
99 | try:
100 | client.get_invoices()
101 | except TelePayError as e:
102 | if e.status_code == 403:
103 | assert e.error == "forbidden"
104 | assert e.message == ERRORS["forbidden"]
105 |
106 |
107 | def test_get_invoice(client: TelePaySyncClient, invoice: Invoice):
108 | try:
109 | client.get_invoice(invoice.number)
110 | except TelePayError as e:
111 | if e.status_code == 403:
112 | assert e.error == "forbidden"
113 | assert e.message == ERRORS["forbidden"]
114 |
115 |
116 | def test_get_invoice_not_found(client: TelePaySyncClient):
117 | number = random_text(10)
118 | try:
119 | client.get_invoice(number)
120 | assert False
121 | except TelePayError as e:
122 | if e.status_code == 403:
123 | assert e.error == "forbidden"
124 | assert e.message == ERRORS["forbidden"]
125 | elif e.status_code == 404:
126 | assert e.error == "invoice.not-found"
127 | assert e.message == ERRORS["invoice.not-found"]
128 |
129 |
130 | def test_cancel_invoice(client: TelePaySyncClient, invoice: Invoice):
131 | try:
132 | client.cancel_invoice(invoice.number)
133 | except TelePayError as e:
134 | if e.status_code == 403:
135 | assert e.error == "forbidden"
136 | assert e.message == ERRORS["forbidden"]
137 | elif e.status_code == 404:
138 | assert e.error == "invoice.not-found"
139 | assert e.message == ERRORS["invoice.not-found"]
140 |
141 |
142 | def test_cancel_invoice_not_found(client: TelePaySyncClient):
143 | number = random_text(10)
144 | try:
145 | client.cancel_invoice(number)
146 | except TelePayError as e:
147 | if e.status_code == 403:
148 | assert e.error == "forbidden"
149 | assert e.message == ERRORS["forbidden"]
150 | elif e.status_code == 404:
151 | assert e.error == "invoice.not-found"
152 | assert e.message == ERRORS["invoice.not-found"]
153 |
154 |
155 | def test_delete_invoice(client: TelePaySyncClient, invoice: Invoice):
156 | try:
157 | client.cancel_invoice(invoice.number)
158 | client.delete_invoice(invoice.number)
159 | except TelePayError as e:
160 | if e.status_code == 403:
161 | assert e.error == "forbidden"
162 | assert e.message == ERRORS["forbidden"]
163 | elif e.status_code == 404:
164 | assert e.error == "invoice.not-found"
165 | assert e.message == ERRORS["invoice.not-found"]
166 |
167 |
168 | def test_delete_invoice_not_found(client: TelePaySyncClient):
169 | number = random_text(10)
170 | try:
171 | client.delete_invoice(number)
172 | assert False
173 | except TelePayError as e:
174 | if e.status_code == 403:
175 | assert e.error == "forbidden"
176 | assert e.message == ERRORS["forbidden"]
177 | elif e.status_code == 404:
178 | assert e.error == "invoice.not-found"
179 | assert e.message == ERRORS["invoice.not-found"]
180 |
181 |
182 | def test_transfer_without_funds(client: TelePaySyncClient):
183 | try:
184 | client.transfer(
185 | asset="TON",
186 | blockchain="TON",
187 | network="testnet",
188 | amount=1,
189 | username="telepay",
190 | )
191 | except TelePayError as e:
192 | if e.status_code == 401:
193 | assert e.error == "transfer.insufficient-funds"
194 | assert e.message == ERRORS["transfer.insufficient-funds"]
195 | elif e.status_code == 403:
196 | assert e.error == "forbidden"
197 | assert e.message == ERRORS["forbidden"]
198 | elif e.status_code == 404:
199 | assert e.error == "invoice.not-found"
200 | assert e.message == ERRORS["invoice.not-found"]
201 |
202 |
203 | def test_transfer_to_wrong_user(client: TelePaySyncClient):
204 | username = random_text(20)
205 | try:
206 | client.transfer(
207 | asset="TON",
208 | blockchain="TON",
209 | network="testnet",
210 | amount=1,
211 | username=username,
212 | )
213 | except TelePayError as e:
214 | if e.status_code == 401:
215 | assert e.error == "transfer.insufficient-funds"
216 | assert e.message == ERRORS["transfer.insufficient-funds"]
217 | elif e.status_code == 403:
218 | assert e.error == "forbidden"
219 | assert e.message == ERRORS["forbidden"]
220 | elif e.status_code == 404:
221 | assert e.error == "account.not-found"
222 | assert e.message == ERRORS["account.not-found"]
223 |
224 |
225 | def test_transfer_to_itself(client: TelePaySyncClient):
226 | account = client.get_me()
227 | username = account.merchant["username"]
228 | try:
229 | client.transfer(
230 | asset="TON",
231 | blockchain="TON",
232 | network="testnet",
233 | amount=1,
234 | username=username,
235 | )
236 | except TelePayError as e:
237 | if e.status_code == 401:
238 | assert e.error == "transfer.not-possible"
239 | assert e.message == ERRORS["transfer.not-possible"]
240 | elif e.status_code == 403:
241 | assert e.error == "forbidden"
242 | assert e.message == ERRORS["forbidden"]
243 |
244 |
245 | def test_withdraw_minimum(client: TelePaySyncClient):
246 | try:
247 | client.get_withdraw_minimum(
248 | asset="TON",
249 | blockchain="TON",
250 | network="testnet",
251 | )
252 | except TelePayError as e:
253 | if e.status_code == 403:
254 | assert e.error == "forbidden"
255 | assert e.message == ERRORS["forbidden"]
256 |
257 |
258 | def test_get_withdraw_fee(client: TelePaySyncClient):
259 | try:
260 | client.get_withdraw_fee(
261 | to_address="EQCKYK7bYBt1t8UmdhImrbiSzC5ijfo_H3Zc_Hk8ksRpOkOk",
262 | asset="TON",
263 | blockchain="TON",
264 | network="testnet",
265 | amount=1,
266 | message="test",
267 | )
268 | except TelePayError as e:
269 | if e.status_code == 403:
270 | assert e.error == "forbidden"
271 | assert e.message == ERRORS["forbidden"]
272 |
273 |
274 | def test_withdraw(client: TelePaySyncClient):
275 | try:
276 | client.withdraw(
277 | to_address="EQCKYK7bYBt1t8UmdhImrbiSzC5ijfo_H3Zc_Hk8ksRpOkOk",
278 | asset="TON",
279 | blockchain="TON",
280 | network="testnet",
281 | amount=1,
282 | message="test",
283 | )
284 | except TelePayError as e:
285 | if e.status_code == 503:
286 | assert e.error == "unavailable"
287 | assert e.message == ERRORS["unavailable"]
288 | elif e.status_code == 401:
289 | assert e.error == "withdrawal.insufficient-funds"
290 | assert e.message == ERRORS["withdrawal.insufficient-funds"]
291 | elif e.status_code == 403:
292 | assert e.error == "forbidden"
293 | assert e.message == ERRORS["forbidden"]
294 |
295 |
296 | def test_update_webhook(client: TelePaySyncClient, webhook: Webhook):
297 | try:
298 | client.update_webhook(
299 | id=webhook.id,
300 | url="https://example.com",
301 | secret="hello",
302 | events=["invoice.completed"],
303 | active=False,
304 | )
305 | client.delete_webhook(id=webhook.id)
306 | except TelePayError as e:
307 | if e.status_code == 403:
308 | assert e.error == "forbidden"
309 | assert e.message == ERRORS["forbidden"]
310 | if e.status_code == 404:
311 | assert e.error == "webhook.not-found"
312 | assert e.message == ERRORS["webhook.not-found"]
313 |
314 |
315 | def test_activate_webhook(client: TelePaySyncClient, webhook: Webhook):
316 | try:
317 | client.activate_webhook(id=webhook.id)
318 | client.delete_webhook(id=webhook.id)
319 | except TelePayError as e:
320 | if e.status_code == 403:
321 | assert e.error == "forbidden"
322 | assert e.message == ERRORS["forbidden"]
323 | if e.status_code == 404:
324 | assert e.error == "webhook.not-found"
325 | assert e.message == ERRORS["webhook.not-found"]
326 |
327 |
328 | def test_deactivate_webhook(client: TelePaySyncClient, webhook: Webhook):
329 | try:
330 | client.deactivate_webhook(id=webhook.id)
331 | client.delete_webhook(id=webhook.id)
332 | except TelePayError as e:
333 | if e.status_code == 403:
334 | assert e.error == "forbidden"
335 | assert e.message == ERRORS["forbidden"]
336 | if e.status_code == 404:
337 | assert e.error == "webhook.not-found"
338 | assert e.message == ERRORS["webhook.not-found"]
339 |
340 |
341 | def test_delete_webhook(client: TelePaySyncClient, webhook: Webhook):
342 | try:
343 | client.delete_webhook(id=webhook.id)
344 | except TelePayError as e:
345 | if e.status_code == 403:
346 | assert e.error == "forbidden"
347 | assert e.message == ERRORS["forbidden"]
348 | if e.status_code == 404:
349 | assert e.error == "webhook.not-found"
350 | assert e.message == ERRORS["webhook.not-found"]
351 |
352 |
353 | def test_get_webhook(client: TelePaySyncClient, webhook: Webhook):
354 | try:
355 | client.get_webhook(id=webhook.id)
356 | client.delete_webhook(id=webhook.id)
357 | except TelePayError as e:
358 | if e.status_code == 403:
359 | assert e.error == "forbidden"
360 | assert e.message == ERRORS["forbidden"]
361 | if e.status_code == 404:
362 | assert e.error == "webhook.not-found"
363 | assert e.message == ERRORS["webhook.not-found"]
364 |
365 |
366 | def test_get_webhooks(client: TelePaySyncClient):
367 | try:
368 | client.get_webhooks()
369 | except TelePayError as e:
370 | if e.status_code == 403:
371 | assert e.error == "forbidden"
372 | assert e.message == ERRORS["forbidden"]
373 | if e.status_code == 404:
374 | assert e.error == "webhook.not-found"
375 | assert e.message == ERRORS["webhook.not-found"]
376 |
--------------------------------------------------------------------------------
/tests/v1/utils.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | ERRORS = {
5 | "forbidden": "You are not authorized to perform this action.",
6 | "unavailable": "This action is temporarly unavailable.",
7 | "account.not-found": "Account not found.",
8 | "invoice.not-found": "Invoice not found.",
9 | "transfer.insufficient-funds": "Transfer failed. Insufficient funds.",
10 | "transfer.not-possible": "Transfer failed. Not possible to perform.",
11 | "withdrawal.insufficient-funds": "Withdrawal failed. Insufficient funds.",
12 | "webhook.not-found": "Webhook not found.",
13 | }
14 |
15 |
16 | def random_text(length):
17 | chars = string.ascii_uppercase + string.digits
18 | return "".join(random.choice(chars) for _ in range(length))
19 |
--------------------------------------------------------------------------------