├── .editorconfig
├── .github
├── dependabot.yaml
└── workflows
│ ├── ci.yml
│ ├── codeql.yml
│ ├── locks.yml
│ └── publish_pypi.yml
├── .gitignore
├── .gitmodules
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── LICENSE
├── README.md
├── benchmarks
└── p2p
│ ├── p2p.svg
│ └── test_performance.py
├── codecov.yaml
├── docs
├── API
│ ├── index.rst
│ ├── qiwi_api.rst
│ ├── qiwi_maps.rst
│ └── yoomoney_api.rst
├── Makefile
├── _static
│ └── logo.png
├── advanced_features
│ ├── cache.rst
│ ├── index.rst
│ ├── known-issues.rst
│ └── proxy.rst
├── code
│ ├── __init__.py
│ ├── polling
│ │ ├── __init__.py
│ │ ├── events.py
│ │ ├── qiwi.py
│ │ ├── with_aiogram.py
│ │ ├── with_aiogram_non_blocking.py
│ │ └── without_globals.py
│ └── webhooks
│ │ ├── __init__.py
│ │ └── qiwi.py
├── conf.py
├── getting-started
│ ├── index.rst
│ ├── qiwi
│ │ ├── examples.rst
│ │ ├── index.rst
│ │ └── usage_with_aiogram.rst
│ └── yoomoney
│ │ ├── examples.rst
│ │ └── index.rst
├── index.rst
├── installation.rst
├── make.bat
├── polling.rst
├── requirements.txt
├── static
│ └── demo.gif
├── types
│ ├── arbitrary
│ │ └── index.rst
│ ├── index.rst
│ ├── qiwi_types
│ │ └── index.rst
│ └── yoomoney_types
│ │ └── index.rst
└── webhooks.rst
├── glQiwiApi
├── __init__.py
├── core
│ ├── __init__.py
│ ├── abc
│ │ ├── __init__.py
│ │ ├── api_method.py
│ │ └── base_api_client.py
│ ├── cache
│ │ ├── __init__.py
│ │ ├── cached_types.py
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ ├── invalidation.py
│ │ ├── storage.py
│ │ └── utils.py
│ ├── event_fetching
│ │ ├── __init__.py
│ │ ├── class_based
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── bill.py
│ │ │ ├── error.py
│ │ │ ├── transaction.py
│ │ │ └── webhook_transaction.py
│ │ ├── dispatcher.py
│ │ ├── executor.py
│ │ ├── filters.py
│ │ └── webhooks
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ ├── config.py
│ │ │ ├── dto
│ │ │ ├── __init__.py
│ │ │ └── errors.py
│ │ │ ├── middlewares
│ │ │ ├── __init__.py
│ │ │ └── ip.py
│ │ │ ├── services
│ │ │ ├── __init__.py
│ │ │ ├── collision_detector.py
│ │ │ └── security
│ │ │ │ ├── __init__.py
│ │ │ │ └── ip.py
│ │ │ ├── utils.py
│ │ │ └── views
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── bill_view.py
│ │ │ └── transaction_view.py
│ ├── request_service.py
│ └── session
│ │ ├── __init__.py
│ │ └── holder.py
├── ext
│ ├── __init__.py
│ └── webhook_url.py
├── py.typed
├── qiwi
│ ├── __init__.py
│ ├── base.py
│ ├── clients
│ │ ├── __init__.py
│ │ ├── maps
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── methods
│ │ │ │ ├── __init__.py
│ │ │ │ ├── get_partners.py
│ │ │ │ └── get_terminals.py
│ │ │ └── types
│ │ │ │ ├── __init__.py
│ │ │ │ ├── polygon.py
│ │ │ │ └── terminal.py
│ │ ├── p2p
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── methods
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_p2p_bill.py
│ │ │ │ ├── create_p2p_key_pair.py
│ │ │ │ ├── get_bill_by_id.py
│ │ │ │ ├── refund_bill.py
│ │ │ │ └── reject_p2p_bill.py
│ │ │ └── types.py
│ │ ├── wallet
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ ├── methods
│ │ │ │ ├── __init__.py
│ │ │ │ ├── authenticate_wallet.py
│ │ │ │ ├── check_restriction.py
│ │ │ │ ├── create_new_balance.py
│ │ │ │ ├── detect_mobile_number.py
│ │ │ │ ├── fetch_statistics.py
│ │ │ │ ├── get_account_info.py
│ │ │ │ ├── get_available_balances.py
│ │ │ │ ├── get_balances.py
│ │ │ │ ├── get_cards.py
│ │ │ │ ├── get_cross_rates.py
│ │ │ │ ├── get_identification.py
│ │ │ │ ├── get_limits.py
│ │ │ │ ├── get_nickname.py
│ │ │ │ ├── get_receipt.py
│ │ │ │ ├── history.py
│ │ │ │ ├── list_of_invoices.py
│ │ │ │ ├── pay_invoice.py
│ │ │ │ ├── payment_by_details.py
│ │ │ │ ├── predict_comission.py
│ │ │ │ ├── qiwi_master
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── block_card.py
│ │ │ │ │ ├── buy_qiwi_card.py
│ │ │ │ │ ├── buy_qiwi_master.py
│ │ │ │ │ ├── confirm_qiwi_master.py
│ │ │ │ │ ├── create_card_purchase_order.py
│ │ │ │ │ ├── get_card_requisites.py
│ │ │ │ │ ├── get_statement.py
│ │ │ │ │ ├── rename_card.py
│ │ │ │ │ └── unblock_card.py
│ │ │ │ ├── reveal_card_id.py
│ │ │ │ ├── set_default_balance.py
│ │ │ │ ├── transaction_info.py
│ │ │ │ ├── transfer_money.py
│ │ │ │ ├── transfer_money_to_card.py
│ │ │ │ └── webhook
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── change_webhook_secret.py
│ │ │ │ │ ├── delete_current_webhook.py
│ │ │ │ │ ├── get_current_webhook.py
│ │ │ │ │ ├── get_webhook_secret.py
│ │ │ │ │ ├── register_webhook.py
│ │ │ │ │ └── send_test_notification.py
│ │ │ └── types
│ │ │ │ ├── __init__.py
│ │ │ │ ├── account_info.py
│ │ │ │ ├── balance.py
│ │ │ │ ├── commission.py
│ │ │ │ ├── identification.py
│ │ │ │ ├── limit.py
│ │ │ │ ├── mobile_operator.py
│ │ │ │ ├── nickname.py
│ │ │ │ ├── other.py
│ │ │ │ ├── partner.py
│ │ │ │ ├── payment_info.py
│ │ │ │ ├── qiwi_master.py
│ │ │ │ ├── restriction.py
│ │ │ │ ├── stats.py
│ │ │ │ ├── transaction.py
│ │ │ │ └── webhooks.py
│ │ └── wrapper.py
│ └── exceptions.py
├── types
│ ├── __init__.py
│ ├── _currencies.py
│ ├── amount.py
│ ├── arbitrary
│ │ ├── __init__.py
│ │ ├── file.py
│ │ └── inputs.py
│ ├── base.py
│ └── exceptions.py
├── utils
│ ├── __init__.py
│ ├── certificates.py
│ ├── compat.py
│ ├── currency_util.py
│ ├── date_conversion.py
│ ├── deprecated.py
│ ├── mypy_hacks.py
│ ├── payload.py
│ ├── synchronous
│ │ ├── __init__.py
│ │ ├── adapter.py
│ │ └── adapter.pyi
│ └── validators.py
└── yoo_money
│ ├── __init__.py
│ ├── client.py
│ ├── exceptions.py
│ ├── methods
│ ├── __init__.py
│ ├── acccept_incoming_transfer.py
│ ├── build_auth_url.py
│ ├── get_access_token.py
│ ├── make_cellular_payment.py
│ ├── operation_details.py
│ ├── operation_history.py
│ ├── process_payment.py
│ ├── reject_incoming_transfer.py
│ ├── request_payment.py
│ ├── retrieve_account_info.py
│ └── revoke_api_token.py
│ └── types.py
├── poetry.lock
├── pyproject.toml
└── tests
├── __init__.py
├── conftest.py
├── integration
├── __init__.py
├── test_qiwi
│ ├── __init__.py
│ └── test_clients
│ │ ├── __init__.py
│ │ ├── test_maps.py
│ │ ├── test_p2p_client.py
│ │ └── test_wallet.py
└── test_yoomoney
│ ├── __init__.py
│ └── test_client.py
├── settings.py
└── unit
├── __init__.py
├── base.py
├── test_api_methods
├── __init__.py
├── test_api_method.py
├── test_qiwi_api_method.py
└── test_runtime_value.py
├── test_errors
└── test_yoomoney_errors.py
├── test_event_fetching
├── __init__.py
├── mocks.py
├── test_executor.py
├── test_filters
│ ├── __init__.py
│ └── test_custom_filters.py
└── test_webhook
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_app.py
│ ├── test_services
│ ├── __init__.py
│ ├── test_collision_detector.py
│ └── test_security.py
│ ├── test_utils.py
│ └── test_views
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_p2p_view.py
│ └── test_txn_view.py
├── test_initialization
├── __init__.py
└── test_qiwi_p2p_client.py
├── test_plugins
├── __init__.py
└── test_telegram_plugins
│ ├── __init__.py
│ ├── test_polling.py
│ └── test_webhook.py
├── test_session
├── __init__.py
└── test_aiohttp_session_holder.py
├── test_types
├── __init__.py
└── test_arbitrary
│ ├── __init__.py
│ └── test_file.py
└── test_utils
├── __init__.py
├── test_api_helper.py
├── test_certificates.py
├── test_currency_parser.py
└── test_executor
├── __init__.py
└── test_webhook.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [**]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | max_line_length = 99
11 |
12 | [**.{yml,yaml}]
13 | indent_size = 2
14 |
15 | [**.json]
16 | indent_size = 2
17 |
18 | [**.{md,txt,rst}]
19 | trim_trailing_whitespace = false
20 |
21 | [Makefile]
22 | indent_style = tab
23 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "dev-2.x", "dev-1.x" ]
6 | pull_request:
7 | branches: [ "dev-2.x" ]
8 | schedule:
9 | - cron: "3 5 * * 0"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ python ]
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v2
31 | with:
32 | languages: ${{ matrix.language }}
33 | queries: +security-and-quality
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v2
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v2
40 | with:
41 | category: "/language:${{ matrix.language }}"
42 |
--------------------------------------------------------------------------------
/.github/workflows/locks.yml:
--------------------------------------------------------------------------------
1 | # Copied from https://github.com/pallets/flask/blob/main/.github/workflows/lock.yaml
2 |
3 | # This does not automatically close "stale" issues. Instead, it locks closed issues after 3 weeks of no activity.
4 | # If there's a new issue related to an old one, we've found it's much easier to work on as a new issue.
5 |
6 | name: 'Lock threads'
7 |
8 | on:
9 | schedule:
10 | - cron: '0 0 * * *'
11 |
12 | jobs:
13 | lock:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: dessant/lock-threads@v3
17 | with:
18 | github-token: ${{ github.token }}
19 | issue-inactive-days: 21
20 | pr-inactive-days: 21
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish_pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish to the Pypi
2 |
3 | on:
4 | push:
5 | tags:
6 | - '2.*'
7 |
8 | jobs:
9 | build:
10 | name: Build
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@master
14 |
15 | - name: Set up Python 3.10
16 | uses: actions/setup-python@v4
17 | with:
18 | python-version: "3.10"
19 |
20 | - name: Install and configure Poetry
21 | uses: snok/install-poetry@v1
22 | with:
23 | version: 1.2.1
24 | virtualenvs-create: false
25 | installer-parallel: true
26 |
27 | - name: Build
28 | run: |
29 | poetry build
30 | - name: Try install wheel
31 | run: |
32 | pip install -U virtualenv
33 | mkdir -p try_install
34 | cd try_install
35 | virtualenv venv
36 | venv/bin/pip install ../dist/glQiwiApi-*.whl
37 | venv/bin/python -c "import glQiwiApi; print(glQiwiApi.__version__)"
38 | - name: Publish artifacts
39 | uses: actions/upload-artifact@v2
40 | with:
41 | name: dist
42 | path: dist/*
43 |
44 | publish:
45 | name: Publish
46 | needs: build
47 | if: "success() && startsWith(github.ref, 'refs/tags/')"
48 | runs-on: ubuntu-latest
49 | steps:
50 | - name: Download artifacts
51 | uses: actions/download-artifact@v1
52 | with:
53 | name: dist
54 | path: dist
55 |
56 | - name: Publish a Python distribution to PyPI
57 | uses: pypa/gh-action-pypi-publish@master
58 | with:
59 | user: __token__
60 | password: ${{ secrets.PYPI_TOKEN }}
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | garbage.py
6 | .vscode
7 | # C ext
8 | *.so
9 | glQiwiApi/test.py
10 | ssl_load_s.py
11 | setup.py
12 | .coverage
13 | htmlcov
14 |
15 | .idea
16 |
17 | .benchmarks
18 |
19 | glQiwiApi/t.py
20 | *.pem
21 | # Distribution / packaging
22 | .Python
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | wheels/
35 | pip-wheel-metadata/
36 | share/python-wheels/
37 | *.egg-info/
38 | .installed.cfg
39 | *.egg
40 | MANIFEST
41 |
42 | # PyInstaller
43 | # Usually these files are written by a python script from a template
44 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
45 | *.manifest
46 | *.spec
47 |
48 | # Installer logs
49 | pip-log.txt
50 | pip-delete-this-directory.txt
51 |
52 | # Unit test / coverage reports
53 | htmlcov/
54 | .tox/
55 | .nox/
56 | .coverage.*
57 | .cache
58 | nosetests.xml
59 | coverage.xml
60 | *.cover
61 | *.py,cover
62 | .hypothesis/
63 | .pytest_cache/
64 |
65 | # Translations
66 | *.mo
67 | *.pot
68 |
69 | # Django stuff:
70 | *.log
71 | local_settings.py
72 | db.sqlite3
73 | db.sqlite3-journal
74 |
75 | # Flask stuff:
76 | instance/
77 | .webassets-cache
78 |
79 | # Scrapy stuff:
80 | .scrapy
81 |
82 | # Sphinx documentation
83 | docs/_build/
84 |
85 | # PyBuilder
86 | target/
87 |
88 | # Jupyter Notebook
89 | .ipynb_checkpoints
90 |
91 | # IPython
92 | profile_default/
93 | ipython_config.py
94 |
95 | # pyenv
96 | .python-version
97 |
98 | # pipenv
99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
102 | # install all needed dependencies.
103 | #Pipfile.lock
104 |
105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
106 | __pypackages__/
107 |
108 | # Celery stuff
109 | celerybeat-schedule
110 | celerybeat.pid
111 |
112 | # SageMath parsed files
113 | *.sage.py
114 |
115 | # Environments
116 | .venv
117 | */.env
118 | env/
119 | venv/
120 | ENV/
121 | env.bak/
122 | venv.bak/
123 |
124 | # Spyder project settings
125 | .spyderproject
126 | .spyproject
127 |
128 | # Rope project settings
129 | .ropeproject
130 |
131 | # mkdocs documentation
132 | /site
133 |
134 | # mypy
135 | .mypy_cache/
136 | .dmypy.json
137 | dmypy.json
138 |
139 | # Pyre type checker
140 | .pyre/
141 | .idea/*
142 | .env
143 |
144 | docker_test/*
145 | docker_test
146 |
147 | .ruff_cache/*
148 | playground.py
149 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "qiwi-referrer-proxy"]
2 | path = qiwi-referrer-proxy
3 | url = https://github.com/GLEF1X/qiwi-referrer-proxy.git
4 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.4.0
4 | hooks:
5 | - id: "trailing-whitespace"
6 | - id: "check-case-conflict"
7 | - id: "check-merge-conflict"
8 | - id: "debug-statements"
9 | - id: "end-of-file-fixer"
10 | - id: "mixed-line-ending"
11 | - id: "check-yaml"
12 | - id: "detect-private-key"
13 | - id: "check-toml"
14 |
15 | - repo: https://github.com/psf/black
16 | rev: 22.12.0
17 | hooks:
18 | - id: black
19 | files: &files '^(glQiwiApi|tests|examples|benchmarks|docs)'
20 |
21 | - repo: https://github.com/pre-commit/mirrors-isort
22 | rev: v5.10.1
23 | hooks:
24 | - id: isort
25 | additional_dependencies: [ toml ]
26 | files: *files
27 |
28 | - repo: https://github.com/floatingpurr/sync_with_poetry
29 | rev: 0.4.0
30 | hooks:
31 | - id: sync_with_poetry
32 |
33 | - repo: https://github.com/charliermarsh/ruff-pre-commit
34 | rev: v0.0.204
35 | hooks:
36 | - id: ruff
37 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | sphinx:
4 | configuration: docs/conf.py
5 |
6 | formats:
7 | - htmlzip
8 | - epub
9 |
10 | python:
11 | version: 3.10
12 | install:
13 | - method: pip
14 | path: .
15 | extra_requirements:
16 | - docs
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 GLEF1X
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://pypi.org/project/glQiwiApi/)  
6 | [](https://lgtm.com/projects/g/GLEF1X/glQiwiApi/context:python) [](https://www.codefactor.io/repository/github/glef1x/glqiwiapi)
7 | 
8 |   [](https://pepy.tech/project/glqiwiapi) [](https://pepy.tech/project/glqiwiapi)
9 |
10 |
11 |
12 |
13 | ## 🌎Official API resources:
14 |
15 | * 🎓 __Documentation: [here](https://glqiwiapi.readthedocs.io/en/latest/)__
16 | * 🖱️ __Telegram chat: [](https://t.me/glQiwiAPIOfficial)__
17 |
18 | ## Benchmarks
19 |
20 | ```bash
21 | hint: smaller is better
22 | glQiwiApi 90.9925 (1.0) 103.3993 (1.0) 95.4082 (1.0) 5.3941 (1.0) 92.4023 (1.0) 8.2798 (1.0)
23 | pyQiwiP2P 112.2819 (1.23) 135.0227 (1.31) 123.7498 (1.30) 9.9919 (1.85) 127.5926 (1.38) 17.2723 (2.09)
24 | ```
25 |
26 | ## 🐦Dependencies
27 |
28 | | Library | Description |
29 | |:-------:|:-------------------------------------------------------:|
30 | |aiohttp | Asynchronous HTTP Client/Server for asyncio and Python. |
31 | |pydantic | Json data validator. Very fast instead of custom |
32 |
--------------------------------------------------------------------------------
/benchmarks/p2p/test_performance.py:
--------------------------------------------------------------------------------
1 | # pip install pytest-benchmark pyqiwip2p
2 | import os
3 |
4 | import pytest
5 | from pyqiwip2p.AioQiwip2p import AioQiwiP2P
6 | from pytest_benchmark.fixture import BenchmarkFixture
7 |
8 | from glQiwiApi import QiwiP2PClient
9 |
10 | wrapper = QiwiP2PClient(secret_p2p=os.getenv('SECRET_P2P'))
11 |
12 | c = AioQiwiP2P(auth_key=os.getenv('SECRET_P2P'))
13 |
14 |
15 | # Results on my machine (smaller is better)
16 | # test_create_bill_with_glQiwiApi 90.9925 (1.0) 103.3993 (1.0) 95.4082 (1.0) 5.3941 (1.0) 92.4023 (1.0) 8.2798 (1.0) 1;0 10.4813 (1.0) 5 11
17 | # test_create_bill_with_pyQiwiP2P 112.2819 (1.23) 135.0227 (1.31) 123.7498 (1.30) 9.9919 (1.85) 127.5926 (1.38) 17.2723 (2.09) 2;0 8.0808 (0.77) 5 10
18 |
19 |
20 | async def create_bill_with_glQiwiApi():
21 | await wrapper.create_p2p_bill(amount=1)
22 |
23 |
24 | async def create_bill_with_pyQiwiP2P():
25 | await c.bill(amount=1)
26 |
27 |
28 | @pytest.fixture()
29 | def aio_benchmark(benchmark: BenchmarkFixture) -> BenchmarkFixture:
30 | import asyncio
31 | import threading
32 |
33 | class Sync2Async:
34 | def __init__(self, coro, *args, **kwargs):
35 | self.coro = coro
36 | self.args = args
37 | self.kwargs = kwargs
38 | self.custom_loop = None
39 | self.thread = None
40 |
41 | def start_background_loop(self) -> None:
42 | asyncio.set_event_loop(self.custom_loop)
43 | self.custom_loop.run_forever()
44 |
45 | def __call__(self):
46 | evloop = None
47 | awaitable = self.coro(*self.args, **self.kwargs)
48 | try:
49 | evloop = asyncio.get_running_loop()
50 | except:
51 | pass
52 | if evloop is None:
53 | return asyncio.run(awaitable)
54 | else:
55 | if not self.custom_loop or not self.thread or not self.thread.is_alive():
56 | self.custom_loop = asyncio.new_event_loop()
57 | self.thread = threading.Thread(target=self.start_background_loop, daemon=True)
58 | self.thread.start()
59 |
60 | return asyncio.run_coroutine_threadsafe(awaitable, self.custom_loop).result()
61 |
62 | def _wrapper(func, *args, **kwargs):
63 | if asyncio.iscoroutinefunction(func):
64 | benchmark(Sync2Async(func, *args, **kwargs))
65 | else:
66 | benchmark(func, *args, **kwargs)
67 |
68 | return _wrapper
69 |
70 |
71 | @pytest.mark.asyncio
72 | async def test_create_bill_with_glQiwiApi(aio_benchmark: BenchmarkFixture) -> None:
73 | @aio_benchmark
74 | async def _():
75 | await create_bill_with_glQiwiApi()
76 |
77 |
78 | @pytest.mark.asyncio
79 | async def test_create_bill_with_pyQiwiP2P(aio_benchmark: BenchmarkFixture) -> None:
80 | @aio_benchmark
81 | async def _():
82 | await create_bill_with_pyQiwiP2P()
83 |
--------------------------------------------------------------------------------
/codecov.yaml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: yes
3 | branch: dev-2.x
4 |
5 | ignore:
6 | - "glQiwiApi/ext"
7 | - "glQiwiApi/utils/mypy_hacks.py"
8 | - "glQiwiApi/utils/compat.py"
9 | - "glQiwiApi/utils/payload.py"
10 | - "glQiwiApi/qiwi/clients/wrapper.py"
11 | - "glQiwiApi/utils/deprecated.py"
12 | - "glQiwiApi/utils/date_conversion.py"
13 | - "glQiwiApi/utils/synchronous.py"
14 | - "glQiwiApi/core/cache"
15 |
16 | coverage:
17 | precision: 2
18 | round: down
19 | range: "70...100"
20 |
21 | parsers:
22 | gcov:
23 | branch_detection:
24 | conditional: yes
25 | loop: yes
26 | method: no
27 | macro: no
28 |
29 | comment:
30 | layout: "reach,diff,flags,tree"
31 | behavior: default
32 | require_changes: no
33 | branches:
34 | - dev-2.x
35 | after_n_builds: 6
36 |
--------------------------------------------------------------------------------
/docs/API/index.rst:
--------------------------------------------------------------------------------
1 | ======================
2 | Available API wrappers
3 | ======================
4 |
5 | The glQiwiApi is capable of interacting with many APIs independently.
6 | Actually, there is no restrictions to usage of both YooMoney and QIWI API.
7 |
8 | .. toctree::
9 | qiwi_api
10 | yoomoney_api
11 | qiwi_maps
12 |
--------------------------------------------------------------------------------
/docs/API/qiwi_api.rst:
--------------------------------------------------------------------------------
1 | ########
2 | QIWI API
3 | ########
4 |
5 |
6 | .. autoclass:: glQiwiApi.qiwi.clients.wrapper.QiwiWrapper
7 | :members:
8 | :show-inheritance:
9 | :member-order: bysource
10 | :special-members: __init__
11 | :undoc-members: True
12 |
13 | .. autoclass:: glQiwiApi.qiwi.clients.wallet.client.QiwiWallet
14 | :members:
15 | :show-inheritance:
16 | :member-order: bysource
17 | :special-members: __init__
18 | :undoc-members: True
19 |
20 | .. autoclass:: glQiwiApi.qiwi.clients.p2p.client.QiwiP2PClient
21 | :members:
22 | :show-inheritance:
23 | :member-order: bysource
24 | :special-members: __init__
25 | :undoc-members: True
26 |
--------------------------------------------------------------------------------
/docs/API/qiwi_maps.rst:
--------------------------------------------------------------------------------
1 | #############
2 | QIWI Maps API
3 | #############
4 |
5 | .. autoclass:: glQiwiApi.qiwi.clients.maps.client.QiwiMaps
6 | :members:
7 | :show-inheritance:
8 | :member-order: bysource
9 | :special-members: __init__
10 | :undoc-members: True
11 |
--------------------------------------------------------------------------------
/docs/API/yoomoney_api.rst:
--------------------------------------------------------------------------------
1 | ############
2 | YooMoney API
3 | ############
4 |
5 | .. autoclass:: glQiwiApi.yoo_money.client.YooMoneyAPI
6 | :members:
7 | :show-inheritance:
8 | :member-order: bysource
9 | :special-members: __init__
10 | :undoc-members: True
11 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SPHINXINTL ?= sphinx-intl
9 | SOURCEDIR = .
10 | BUILDDIR = _build
11 |
12 | # Put it first so that "make" without argument is like "make help".
13 | help:
14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
15 |
16 | .PHONY: help update-intl Makefile
17 |
18 | # Catch-all target: route all unknown targets to Sphinx using the new
19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
20 | %: Makefile
21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
22 |
--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/docs/_static/logo.png
--------------------------------------------------------------------------------
/docs/advanced_features/cache.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Query caching
3 | =============
4 |
5 | glQiwiApi provides builtin cache storage and cache invalidation strategies
6 | such a `InMemoryCacheStorage` and `APIResponsesCacheInvalidationStrategy` accordingly.
7 | Obviously, you can extend functionality of library and add, for example, Redis based cache storage.
8 |
9 |
10 |
11 | .. tip:: By default InMemoryCacheStorage use `UnrealizedCacheInvalidationStrategy` as an invalidation strategy
12 |
13 | Straightforward example
14 | -----------------------
15 |
16 | .. code-block:: python
17 |
18 | import asyncio
19 |
20 | from glQiwiApi.core.cache import InMemoryCacheStorage
21 |
22 | storage = InMemoryCacheStorage() # here is UnrealizedCacheInvalidationStrategy as an invalidation strategy
23 |
24 |
25 | async def cache():
26 | await storage.update(cached=5)
27 | cached = await storage.retrieve("cached")
28 | print(f"You have cached {cached}")
29 |
30 |
31 | asyncio.run(cache())
32 |
33 | Advanced usage
34 | --------------
35 |
36 | This very cache invalidation by timer strategy is worth using if you want to achieve cache invalidation.
37 | It should be noted that previous `UnrealizedCacheInvalidationStrategy` just ignores the invalidation and don't get rid of aged cache.
38 |
39 | .. code-block:: python
40 |
41 | import asyncio
42 |
43 | from glQiwiApi.core.cache import InMemoryCacheStorage, CacheInvalidationByTimerStrategy
44 |
45 | storage = InMemoryCacheStorage(CacheInvalidationByTimerStrategy(cache_time_in_seconds=1))
46 |
47 |
48 | async def main():
49 | await storage.update(x=5)
50 |
51 | await asyncio.sleep(1)
52 |
53 | value = await storage.retrieve("x") # None
54 |
55 |
56 | asyncio.run(main())
57 |
--------------------------------------------------------------------------------
/docs/advanced_features/index.rst:
--------------------------------------------------------------------------------
1 | ======================
2 | Low level API features
3 | ======================
4 |
5 | .. toctree::
6 | cache
7 | proxy
8 | known-issues
9 |
--------------------------------------------------------------------------------
/docs/advanced_features/known-issues.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Known issues:
3 | =============
4 |
5 |
6 | ```
7 | aiohttp.client_exceptions.ClientConnectorError: Cannot connect to api.qiwi.com ssl:true...
8 | ```
9 |
10 | This exception can be fixed this way:
11 |
12 | .. code-block:: python
13 |
14 | import glQiwiApi
15 | from glQiwiApi import QiwiWallet
16 | from glQiwiApi.core import RequestService
17 | from glQiwiApi.core.session import AiohttpSessionHolder
18 |
19 |
20 | async def create_request_service(w: QiwiWallet):
21 | return RequestService(
22 | session_holder=AiohttpSessionHolder(
23 | headers={
24 | "Content-Type": "application/json",
25 | "Accept": "application/json",
26 | "Authorization": f"Bearer {w._api_access_token}",
27 | "Host": "edge.qiwi.com",
28 | "User-Agent": f"glQiwiApi/{glQiwiApi.__version__}",
29 | },
30 | trust_env=True
31 | )
32 | )
33 |
34 |
35 | wallet = QiwiWallet(request_service_factory=create_request_service)
36 |
--------------------------------------------------------------------------------
/docs/advanced_features/proxy.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | Using proxy
3 | ===========
4 |
5 |
6 | In the example below we'll use `aiohttp-socks` library to establish socks5 proxy:
7 |
8 |
9 | .. code-block:: python
10 |
11 | import asyncio
12 |
13 | from aiohttp_socks import ProxyConnector
14 |
15 | from glQiwiApi import QiwiWallet
16 | from glQiwiApi.core import RequestService
17 | from glQiwiApi.core.session import AiohttpSessionHolder
18 |
19 |
20 | def create_request_service_with_proxy(w: QiwiWallet):
21 | return RequestService(
22 | session_holder=AiohttpSessionHolder(
23 | connector=ProxyConnector.from_url("socks5://34.134.60.185:443"), # some proxy
24 | headers={
25 | "Content-Type": "application/json",
26 | "Accept": "application/json",
27 | "Authorization": f"Bearer {w._api_access_token}",
28 | "Host": "edge.qiwi.com",
29 | },
30 | )
31 | )
32 |
33 |
34 | wallet = QiwiWallet(
35 | api_access_token="your token",
36 | phone_number="+phone number",
37 | request_service_factory=create_request_service_with_proxy,
38 | )
39 |
40 |
41 | async def main():
42 | async with wallet:
43 | print(await wallet.get_balance())
44 |
45 |
46 | asyncio.run(main())
47 |
--------------------------------------------------------------------------------
/docs/code/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/docs/code/__init__.py
--------------------------------------------------------------------------------
/docs/code/polling/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/docs/code/polling/__init__.py
--------------------------------------------------------------------------------
/docs/code/polling/events.py:
--------------------------------------------------------------------------------
1 | from glQiwiApi import QiwiWallet
2 | from glQiwiApi.core.event_fetching import executor
3 | from glQiwiApi.core.event_fetching.dispatcher import QiwiDispatcher
4 | from glQiwiApi.core.event_fetching.executor import HandlerContext
5 | from glQiwiApi.qiwi.clients.wallet.types import Transaction
6 |
7 | qiwi_dp = QiwiDispatcher()
8 | wallet = QiwiWallet(api_access_token='token', phone_number='+phone number')
9 |
10 |
11 | @qiwi_dp.transaction_handler()
12 | async def handle_transaction(t: Transaction, ctx: HandlerContext):
13 | """Handle transaction here"""
14 | ctx.wallet # this way you can use QiwiWallet instance to avoid global variables
15 |
16 |
17 | async def on_startup(ctx: HandlerContext):
18 | ctx.wallet # do something here
19 |
20 |
21 | async def on_shutdown(ctx: HandlerContext):
22 | pass
23 |
24 |
25 | if __name__ == '__main__':
26 | executor.start_polling(wallet, qiwi_dp, on_startup=on_startup, on_shutdown=on_shutdown)
27 |
--------------------------------------------------------------------------------
/docs/code/polling/qiwi.py:
--------------------------------------------------------------------------------
1 | from glQiwiApi import QiwiWallet
2 | from glQiwiApi.core.event_fetching import executor
3 | from glQiwiApi.core.event_fetching.dispatcher import QiwiDispatcher
4 | from glQiwiApi.core.event_fetching.executor import HandlerContext
5 | from glQiwiApi.core.event_fetching.filters import ExceptionFilter
6 | from glQiwiApi.qiwi.clients.wallet.types import Transaction
7 | from glQiwiApi.qiwi.exceptions import QiwiAPIError
8 |
9 | qiwi_dp = QiwiDispatcher()
10 | wallet = QiwiWallet(api_access_token='token', phone_number='+phone number')
11 |
12 |
13 | @qiwi_dp.transaction_handler()
14 | async def handle_transaction(t: Transaction, ctx: HandlerContext):
15 | """Handle transaction here"""
16 | ctx.wallet # this way you can use QiwiWallet instance to avoid global variables
17 |
18 |
19 | @qiwi_dp.exception_handler(ExceptionFilter(QiwiAPIError))
20 | async def handle_exception(err: QiwiAPIError, ctx: HandlerContext):
21 | pass
22 |
23 |
24 | if __name__ == '__main__':
25 | executor.start_polling(wallet, qiwi_dp)
26 |
--------------------------------------------------------------------------------
/docs/code/polling/with_aiogram.py:
--------------------------------------------------------------------------------
1 | from aiogram import Bot, Dispatcher
2 | from aiogram.types import Message
3 |
4 | from glQiwiApi import QiwiWallet
5 | from glQiwiApi.core.event_fetching import executor
6 | from glQiwiApi.core.event_fetching.dispatcher import QiwiDispatcher
7 | from glQiwiApi.core.event_fetching.executor import HandlerContext
8 | from glQiwiApi.plugins import AiogramPollingPlugin
9 | from glQiwiApi.qiwi.clients.wallet.types import Transaction
10 |
11 | qiwi_dp = QiwiDispatcher()
12 | wallet = QiwiWallet(api_access_token='token', phone_number='+phone number')
13 |
14 | dp = Dispatcher(Bot('BOT TOKEN'))
15 |
16 |
17 | @qiwi_dp.transaction_handler()
18 | async def handle_transaction(t: Transaction, ctx: HandlerContext):
19 | """Handle transaction here"""
20 | ctx.wallet # this way you can use QiwiWallet instance to avoid global variables
21 |
22 |
23 | @dp.message_handler()
24 | async def handle_message(msg: Message):
25 | await msg.answer(text='Hello world')
26 |
27 |
28 | if __name__ == '__main__':
29 | executor.start_polling(wallet, qiwi_dp, AiogramPollingPlugin(dp))
30 |
--------------------------------------------------------------------------------
/docs/code/polling/with_aiogram_non_blocking.py:
--------------------------------------------------------------------------------
1 | from aiogram import Bot, Dispatcher
2 | from aiogram.types import Message
3 | from aiogram.utils import executor
4 |
5 | from glQiwiApi import QiwiWallet
6 | from glQiwiApi.core.event_fetching.dispatcher import QiwiDispatcher
7 | from glQiwiApi.core.event_fetching.executor import (
8 | HandlerContext,
9 | start_non_blocking_qiwi_api_polling,
10 | )
11 | from glQiwiApi.qiwi.clients.wallet.types import Transaction
12 |
13 | qiwi_dp = QiwiDispatcher()
14 | wallet = QiwiWallet(api_access_token='token', phone_number='+phone number')
15 |
16 | dp = Dispatcher(Bot('BOT TOKEN'))
17 |
18 |
19 | @qiwi_dp.transaction_handler()
20 | async def handle_transaction(t: Transaction, ctx: HandlerContext):
21 | """Handle transaction here"""
22 |
23 |
24 | @dp.message_handler()
25 | async def handle_message(msg: Message):
26 | await msg.answer(text='Hello world')
27 |
28 |
29 | async def on_startup(dp: Dispatcher):
30 | await start_non_blocking_qiwi_api_polling(wallet, qiwi_dp)
31 |
32 |
33 | if __name__ == '__main__':
34 | executor.start_polling(dp, on_startup=on_startup)
35 |
--------------------------------------------------------------------------------
/docs/code/polling/without_globals.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import cast
3 |
4 | from aiogram import Bot, Dispatcher, types
5 |
6 | from glQiwiApi import QiwiWrapper
7 | from glQiwiApi.core.event_fetching import executor
8 | from glQiwiApi.core.event_fetching.dispatcher import QiwiDispatcher
9 | from glQiwiApi.core.event_fetching.executor import HandlerContext
10 | from glQiwiApi.plugins import AiogramPollingPlugin
11 | from glQiwiApi.qiwi.clients.wallet.types import Transaction
12 |
13 | api_access_token = 'token'
14 | phone_number = '+phone number'
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | async def aiogram_message_handler(msg: types.Message):
20 | await msg.answer(text='Привет😇')
21 |
22 |
23 | async def qiwi_transaction_handler(update: Transaction, ctx: HandlerContext):
24 | print(update)
25 |
26 |
27 | def on_startup(ctx: HandlerContext) -> None:
28 | logger.info('This message logged on startup')
29 | register_handlers(ctx)
30 |
31 |
32 | def register_handlers(ctx: HandlerContext):
33 | ctx['qiwi_dp'].transaction_handler()(qiwi_transaction_handler)
34 | dispatcher = cast(Dispatcher, ctx['dp'])
35 | dispatcher.register_message_handler(aiogram_message_handler)
36 |
37 |
38 | def run_application() -> None:
39 | logging.basicConfig(level=logging.INFO)
40 | bot = Bot('BOT TOKEN')
41 | dp = Dispatcher(bot)
42 | wallet = QiwiWrapper(api_access_token=api_access_token, phone_number=phone_number)
43 | qiwi_dp = QiwiDispatcher()
44 |
45 | executor.start_polling(
46 | wallet,
47 | qiwi_dp,
48 | AiogramPollingPlugin(dp),
49 | on_startup=on_startup,
50 | skip_updates=True,
51 | context={'dp': dp, 'qiwi_dp': qiwi_dp},
52 | )
53 |
54 |
55 | if __name__ == '__main__':
56 | run_application()
57 |
--------------------------------------------------------------------------------
/docs/code/webhooks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/docs/code/webhooks/__init__.py
--------------------------------------------------------------------------------
/docs/code/webhooks/qiwi.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from aiogram import Bot, Dispatcher
4 | from aiogram.dispatcher.webhook import configure_app
5 | from aiohttp import web
6 |
7 | from glQiwiApi import QiwiWallet
8 | from glQiwiApi.core.event_fetching.dispatcher import QiwiDispatcher
9 | from glQiwiApi.core.event_fetching.executor import HandlerContext, configure_app_for_qiwi_webhooks
10 | from glQiwiApi.core.event_fetching.webhooks.config import (
11 | EncryptionConfig,
12 | HookRegistrationConfig,
13 | WebhookConfig,
14 | )
15 | from glQiwiApi.qiwi.clients.p2p.types import BillWebhook
16 |
17 | qiwi_dp = QiwiDispatcher()
18 |
19 | dp = Dispatcher(Bot('BOT TOKEN'))
20 | wallet = QiwiWallet(api_access_token='wallet api token')
21 |
22 |
23 | @qiwi_dp.bill_handler()
24 | async def handle_webhook(webhook: BillWebhook, ctx: HandlerContext):
25 | # handle bill
26 | bill = webhook.bill
27 |
28 |
29 | app = web.Application()
30 | configure_app(
31 | dp,
32 | configure_app_for_qiwi_webhooks(
33 | wallet,
34 | qiwi_dp,
35 | app,
36 | WebhookConfig(
37 | encryption=EncryptionConfig(
38 | secret_p2p_key='secret p2p token, который был зарегистрирован с указанием айпи. '
39 | 'Например http://айпи:8080/webhooks/qiwi/bills/'
40 | ),
41 | hook_registration=HookRegistrationConfig(host_or_ip_address='айпи:8080'),
42 | ),
43 | ),
44 | '/bot',
45 | )
46 |
47 | logging.basicConfig(level=logging.DEBUG)
48 |
49 | if __name__ == '__main__':
50 | # Порт может быть любым
51 | web.run_app(app, port=8080)
52 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import datetime
5 |
6 | import glQiwiApi
7 |
8 | project = 'glQiwiApi'
9 | author = 'GLEF1X'
10 | copyright = f'{datetime.date.today().year}, {author}'
11 | release = glQiwiApi.__version__
12 |
13 | # Add any paths that contain templates here, relative to this directory.
14 | templates_path = ['_templates']
15 |
16 | html_theme = 'furo'
17 | html_logo = '_static/logo.png'
18 | html_static_path = ['_static']
19 | todo_include_todos = True
20 |
21 | extensions = [
22 | 'sphinx.ext.autodoc',
23 | 'sphinx.ext.autodoc.typehints',
24 | 'sphinx.ext.doctest',
25 | 'sphinx.ext.intersphinx',
26 | 'sphinx.ext.viewcode',
27 | 'sphinxemoji.sphinxemoji',
28 | ]
29 |
30 | htmlhelp_basename = project
31 | html_theme_options = {}
32 | html_css_files = [
33 | 'stylesheets/extra.css',
34 | ]
35 |
36 | rst_prolog = f"""
37 | .. role:: pycode(code)
38 | :language: python3
39 | """
40 |
41 | language = None
42 | locale_dirs = ['locales']
43 |
44 | exclude_patterns = []
45 | source_suffix = '.rst'
46 | master_doc = 'index'
47 |
48 | # If true, '()' will be appended to :func: etc. cross-reference text.
49 | add_function_parentheses = True
50 |
51 | latex_documents = [
52 | (master_doc, f'{project}.tex', f'{project} Documentation', author, 'manual'),
53 | ]
54 |
55 | man_pages = [(master_doc, project, f'{project} Documentation', [author], 1)]
56 |
57 | texinfo_documents = [
58 | (
59 | master_doc,
60 | project,
61 | f'{project} Documentation',
62 | author,
63 | project,
64 | 'Modern and fully asynchronous framework for Telegram Bot API',
65 | 'Miscellaneous',
66 | ),
67 | ]
68 |
--------------------------------------------------------------------------------
/docs/getting-started/index.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Getting started
3 | ===============
4 |
5 | .. toctree::
6 | qiwi/index
7 | yoomoney/index
8 |
--------------------------------------------------------------------------------
/docs/getting-started/qiwi/index.rst:
--------------------------------------------------------------------------------
1 | ====
2 | QIWI
3 | ====
4 |
5 | .. toctree::
6 | examples
7 | usage_with_aiogram
8 |
--------------------------------------------------------------------------------
/docs/getting-started/yoomoney/index.rst:
--------------------------------------------------------------------------------
1 | ========
2 | YooMoney
3 | ========
4 |
5 | .. toctree::
6 | examples
7 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 |
8 | .. tip:: Supported Python versions: `3.7` and higher
9 |
10 | Stable release
11 | ----------------
12 |
13 | In order to install glQiwiApi, run this command in your terminal:
14 |
15 | .. code-block:: console
16 |
17 | $ pip install glQiwiApi
18 |
19 | This is the preferred installation method as it will always install the most recent stable release.
20 | If you do not have installed `pip`_, this `Python Installation Reference`_ can help you in the process.
21 |
22 | .. _pip: https://pip.pypa.io
23 | .. _Python Installation Reference: http://docs.python-guide.org/en/latest/starting/installation/
24 |
25 |
26 | From source files
27 | -----------------
28 |
29 | You can either clone the public repository:
30 |
31 | .. code-block:: console
32 |
33 | $ git clone -b dev-2.x git://github.com/GLEF1X/glQiwiApi
34 |
35 | Once you get a copy of the source files, you can install them with:
36 |
37 | .. code-block:: console
38 |
39 | $ python setup.py install
40 |
41 |
42 | Recommendations
43 | ---------------
44 | You can hasten api by following the instructions below:
45 |
46 | - Use `uvloop `_ instead of default asyncio loop.
47 |
48 | *uvloop* is a rapid, drop-in replacement of the built-in asyncio event loop. uvloop is implemented in Cython and uses libuv under the hood.
49 |
50 | **Installation:**
51 |
52 | .. code-block:: bash
53 |
54 | $ pip install uvloop
55 |
56 | - Use `orjson `_ instead of the default json module.
57 |
58 | orjson is a rapid, correct JSON library for Python. It benchmarks as the fastest Python library for JSON and is more correct than the standard json library or other third-party libraries
59 |
60 | **Installation:**
61 |
62 | .. code-block:: bash
63 |
64 | $ pip install orjson
65 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/polling.rst:
--------------------------------------------------------------------------------
1 | ================
2 | Polling updates
3 | ================
4 |
5 | API internals
6 | ~~~~~~~~~~~~~
7 |
8 | .. automodule:: glQiwiApi.core.event_fetching.executor
9 | :members: start_polling,start_non_blocking_qiwi_api_polling,configure_app_for_qiwi_webhooks
10 | :show-inheritance:
11 | :member-order: bysource
12 | :undoc-members: True
13 |
14 | .. autoclass:: glQiwiApi.core.event_fetching.executor.BaseExecutor
15 | :members:
16 | :show-inheritance:
17 | :member-order: bysource
18 | :special-members: __init__
19 | :undoc-members: True
20 |
21 | .. autoclass:: glQiwiApi.core.event_fetching.executor.PollingExecutor
22 | :members:
23 | :show-inheritance:
24 | :member-order: bysource
25 | :special-members: __init__
26 | :undoc-members: True
27 |
28 | Guidelines
29 | ~~~~~~~~~~
30 |
31 | This section explains how to properly poll transactions from QIWI API.
32 |
33 |
34 | *You can't help registering handlers and start polling, so in example above it is shown how to do it rightly.*
35 | Lets do it with decorators:
36 |
37 | .. literalinclude:: code/qiwi/polling.py
38 | :language: python
39 | :emphasize-lines: 15,21
40 |
41 | ️So, we also have to import ``executor`` and pass on our client,
42 | that contains functions ``start_polling`` and ``start_webhook``.
43 |
44 | .. literalinclude:: code/polling/qiwi.py
45 | :language: python
46 | :emphasize-lines: 2
47 |
48 |
49 | Events
50 | ~~~~~~
51 |
52 | Then, you can start polling, but, let's make it clear which arguments you should pass on to ``start_polling`` function.
53 | You can also specify events like ``on_shutdown`` or ``on_startup``.
54 | As you can see, in the example we have a function that we pass as an argument to ``on_startup``.
55 | As you may have guessed, this function will be executed at the beginning of the polling.
56 |
57 | .. literalinclude:: code/polling/events.py
58 | :language: python
59 | :emphasize-lines: 17,21,26
60 |
61 |
62 | Make aiogram work with glQiwiApi
63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
64 |
65 | *Also, you can very easily implement simultaneous polling of updates from both aiogram and QIWI API.*
66 |
67 | In the example below, we catch all text messages and return the same "Hello" response.
68 |
69 | .. literalinclude:: code/polling/with_aiogram.py
70 | :language: python
71 |
72 | Alternatively you can run polling at ``on_startup`` event
73 |
74 | .. literalinclude:: code/polling/with_aiogram_non_blocking.py
75 | :language: python
76 |
77 | Example usage without global variables
78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79 |
80 | .. literalinclude:: code/polling/without_globals.py
81 | :language: python
82 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | furo
2 | markdown-include
3 | pygments
4 | sphinx-autobuild
5 | sphinx-copybutton
6 | sphinx-intl
7 | sphinx-intl
8 | sphinx-notfound-page
9 | sphinx-prompt
10 | Sphinx-Substitution-Extensions
11 | sphinxcontrib-mermaid
12 | sphinxemoji
13 | towncrier
14 | typing-extensions
15 |
--------------------------------------------------------------------------------
/docs/static/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/docs/static/demo.gif
--------------------------------------------------------------------------------
/docs/types/arbitrary/index.rst:
--------------------------------------------------------------------------------
1 | =======================
2 | Custom(arbitrary) types
3 | =======================
4 |
5 | .. note:: When you use method `QiwiWallet.get_receipt` it returns a custom type File, that helps you to save receipt or get raw bytes
6 |
7 | .. autoclass:: glQiwiApi.types.arbitrary.file.File
8 | :members:
9 |
10 |
11 |
12 | .. automodule:: glQiwiApi.types.arbitrary.inputs
13 | :members:
14 |
--------------------------------------------------------------------------------
/docs/types/index.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Types
3 | =====
4 |
5 | .. toctree::
6 | qiwi_types/index
7 | yoomoney_types/index
8 | arbitrary/index
9 |
--------------------------------------------------------------------------------
/docs/types/qiwi_types/index.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | Qiwi types
3 | ==========
4 |
5 | .. automodule:: glQiwiApi.qiwi.clients.wallet.types
6 | :members:
7 |
8 |
9 | .. automodule:: glQiwiApi.qiwi.clients.p2p.types
10 | :members:
11 |
12 |
13 | .. automodule:: glQiwiApi.qiwi.clients.maps.types
14 | :members:
15 |
--------------------------------------------------------------------------------
/docs/types/yoomoney_types/index.rst:
--------------------------------------------------------------------------------
1 | ==============
2 | YooMoney types
3 | ==============
4 |
5 | .. automodule:: glQiwiApi.yoo_money.types.types
6 | :members:
7 |
--------------------------------------------------------------------------------
/docs/webhooks.rst:
--------------------------------------------------------------------------------
1 | ========
2 | Webhooks
3 | ========
4 |
5 |
6 | Quick example:
7 |
8 | .. literalinclude:: code/webhooks/qiwi.py
9 | :language: python
10 |
--------------------------------------------------------------------------------
/glQiwiApi/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from glQiwiApi.qiwi.clients.maps.client import QiwiMaps
4 | from glQiwiApi.qiwi.clients.p2p.client import QiwiP2PClient
5 | from glQiwiApi.qiwi.clients.wallet.client import QiwiWallet
6 | from glQiwiApi.qiwi.clients.wrapper import QiwiWrapper
7 |
8 | from .core.cache import APIResponsesCacheInvalidationStrategy, InMemoryCacheStorage
9 | from .core.cache.storage import CacheStorage
10 | from .yoo_money import YooMoneyAPI
11 |
12 |
13 | def default_cache_storage() -> CacheStorage:
14 | return InMemoryCacheStorage(invalidate_strategy=APIResponsesCacheInvalidationStrategy())
15 |
16 |
17 | if sys.version_info >= (3, 8):
18 | from importlib import metadata
19 | else:
20 | import importlib_metadata as metadata
21 |
22 | try:
23 | __version__ = metadata.version('glQiwiApi')
24 | except metadata.PackageNotFoundError:
25 | __version__ = '99.99.99'
26 | try:
27 | import uvloop as _uvloop
28 |
29 | _uvloop.install()
30 | except ImportError: # pragma: no cover
31 | pass
32 |
33 | __all__ = (
34 | # clients
35 | 'YooMoneyAPI',
36 | 'QiwiMaps',
37 | 'QiwiWallet',
38 | 'QiwiP2PClient',
39 | 'QiwiWrapper',
40 | # other
41 | '__version__',
42 | 'default_cache_storage',
43 | )
44 |
--------------------------------------------------------------------------------
/glQiwiApi/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/core/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/core/abc/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/glQiwiApi/core/cache/__init__.py:
--------------------------------------------------------------------------------
1 | from .invalidation import (
2 | APIResponsesCacheInvalidationStrategy,
3 | CacheInvalidationByTimerStrategy,
4 | CacheInvalidationStrategy,
5 | UnrealizedCacheInvalidationStrategy,
6 | )
7 | from .storage import InMemoryCacheStorage
8 |
--------------------------------------------------------------------------------
/glQiwiApi/core/cache/cached_types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import http
4 | from dataclasses import dataclass
5 | from typing import Any, Dict, Optional, Tuple, Union
6 |
7 |
8 | class Payload:
9 | def __init__(
10 | self,
11 | headers: Optional[Dict[Any, Any]] = None,
12 | json: Optional[Dict[Any, Any]] = None,
13 | params: Optional[Dict[Any, Any]] = None,
14 | data: Optional[Dict[Any, Any]] = None,
15 | **kwargs: Any,
16 | ) -> None:
17 | self.headers = headers
18 | self.json = json
19 | self.params = params
20 | self.data = data
21 |
22 | @classmethod
23 | def new(cls, kwargs: Dict[Any, Any], args: Tuple[Any, ...]) -> Payload:
24 | return cls(**{k: kwargs.get(k) for k in args if isinstance(kwargs.get(k), dict)})
25 |
26 |
27 | @dataclass(frozen=True)
28 | class CachedAPIRequest:
29 | payload: Payload
30 | response: Any
31 | method: Union[str, http.HTTPStatus]
32 |
--------------------------------------------------------------------------------
/glQiwiApi/core/cache/constants.py:
--------------------------------------------------------------------------------
1 | ADD_TIME_PLACEHOLDER = 'add_time'
2 | VALUE_PLACEHOLDER = 'value'
3 | UNCACHED = ('https://api.qiwi.com/partner/bill', '/sinap/api/v2/terms/')
4 |
--------------------------------------------------------------------------------
/glQiwiApi/core/cache/exceptions.py:
--------------------------------------------------------------------------------
1 | class CacheExpiredError(Exception):
2 | pass
3 |
4 |
5 | class CacheValidationError(Exception):
6 | pass
7 |
--------------------------------------------------------------------------------
/glQiwiApi/core/cache/storage.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from typing import Any, Dict, List, Optional
3 |
4 | from glQiwiApi.core.cache.constants import VALUE_PLACEHOLDER
5 | from glQiwiApi.core.cache.exceptions import CacheExpiredError, CacheValidationError
6 | from glQiwiApi.core.cache.invalidation import (
7 | CacheInvalidationStrategy,
8 | UnrealizedCacheInvalidationStrategy,
9 | )
10 | from glQiwiApi.core.cache.utils import embed_cache_time
11 |
12 |
13 | class CacheStorage(abc.ABC):
14 | def __init__(self, invalidate_strategy: Optional[CacheInvalidationStrategy] = None):
15 | if invalidate_strategy is None:
16 | invalidate_strategy = UnrealizedCacheInvalidationStrategy()
17 | self._invalidate_strategy: CacheInvalidationStrategy = invalidate_strategy
18 |
19 | @abc.abstractmethod
20 | async def clear(self) -> None:
21 | pass
22 |
23 | @abc.abstractmethod
24 | async def update(self, **kwargs: Any) -> None:
25 | pass
26 |
27 | @abc.abstractmethod
28 | async def retrieve(self, key: str) -> Any:
29 | pass
30 |
31 | @abc.abstractmethod
32 | async def delete(self, key: str) -> None:
33 | pass
34 |
35 | @abc.abstractmethod
36 | async def retrieve_all(self) -> List[Any]:
37 | ...
38 |
39 | @abc.abstractmethod
40 | async def contains_similar(self, item: Any) -> bool:
41 | ...
42 |
43 | def __getitem__(self, item: Any) -> Any:
44 | return self.retrieve(item)
45 |
46 | def __setitem__(self, key: str, value: Any) -> Any:
47 | self.update(**{key: value})
48 |
49 |
50 | class InMemoryCacheStorage(CacheStorage):
51 | __slots__ = ('_data', '_invalidate_strategy')
52 |
53 | def __init__(self, invalidate_strategy: Optional[CacheInvalidationStrategy] = None):
54 | CacheStorage.__init__(self, invalidate_strategy)
55 | self._data: Dict[Any, Any] = {}
56 |
57 | async def clear(self) -> None:
58 | await self._invalidate_strategy.process_delete()
59 | self._data.clear()
60 |
61 | async def retrieve_all(self) -> List[Optional[Any]]:
62 | return [self.retrieve(key) for key in list(self._data.keys())]
63 |
64 | async def update(self, **kwargs: Any) -> None:
65 | try:
66 | await self._invalidate_strategy.process_update(**kwargs)
67 | except CacheValidationError:
68 | return None
69 | embedded_data: Dict[Any, Any] = embed_cache_time(**kwargs)
70 | self._data.update(embedded_data)
71 |
72 | async def retrieve(self, key: str) -> Optional[Any]:
73 | obj = self._data.get(key)
74 | if obj is None:
75 | return obj
76 | try:
77 | await self._invalidate_strategy.process_retrieve(obj=obj)
78 | return obj[VALUE_PLACEHOLDER]
79 | except CacheExpiredError:
80 | await self.delete(key)
81 | return None
82 |
83 | async def delete(self, key: str) -> None:
84 | del self._data[key]
85 |
86 | async def contains_similar(self, item: Any) -> bool:
87 | return await self._invalidate_strategy.check_is_contains_similar(self, item)
88 |
89 | def __del__(self) -> None:
90 | del self._data
91 |
--------------------------------------------------------------------------------
/glQiwiApi/core/cache/utils.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Any, Dict
3 |
4 | from glQiwiApi.core.cache.constants import ADD_TIME_PLACEHOLDER, VALUE_PLACEHOLDER
5 |
6 |
7 | def embed_cache_time(**kwargs: Any) -> Dict[Any, Any]:
8 | return {
9 | key: {VALUE_PLACEHOLDER: value, ADD_TIME_PLACEHOLDER: time.monotonic()}
10 | for key, value in kwargs.items()
11 | }
12 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/core/event_fetching/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/class_based/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import Handler
2 | from .bill import AbstractBillHandler
3 | from .error import ErrorHandler
4 | from .transaction import AbstractTransactionHandler
5 | from .webhook_transaction import AbstractTransactionWebhookHandler
6 |
7 | __all__ = (
8 | 'AbstractTransactionHandler',
9 | 'AbstractBillHandler',
10 | 'Handler',
11 | 'AbstractTransactionWebhookHandler',
12 | 'ErrorHandler',
13 | )
14 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/class_based/base.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | from typing import TYPE_CHECKING, Any, Dict, Generic, TypeVar, Union
5 |
6 | if TYPE_CHECKING:
7 | from glQiwiApi.types.base import Base # NOQA # pragma: no cover
8 |
9 | T = TypeVar('T', bound=Union[Exception, 'Base'])
10 |
11 |
12 | class BaseHandlerMixin(Generic[T]):
13 | if TYPE_CHECKING: # pragma: no cover
14 | event: T
15 |
16 |
17 | class Handler(abc.ABC, BaseHandlerMixin[T]):
18 | """Base class for all class-based handlers"""
19 |
20 | def __init__(self, event: T, *args: Any) -> None:
21 | self.event: T = event
22 | self._args = args
23 | if args:
24 | self.context: Dict[str, Any] = args[0].context
25 |
26 | @abc.abstractmethod
27 | async def process_event(self) -> Any: # pragma: no cover # type: ignore
28 | raise NotImplementedError
29 |
30 | def __await__(self) -> Any:
31 | return self.process_event().__await__()
32 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/class_based/bill.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | from typing import TYPE_CHECKING, cast
5 |
6 | from glQiwiApi.core.event_fetching.class_based.base import Handler
7 | from glQiwiApi.qiwi.clients.p2p.types import Bill
8 | from glQiwiApi.types.amount import PlainAmount
9 |
10 | if TYPE_CHECKING:
11 | from glQiwiApi.qiwi.clients.p2p.client import QiwiP2PClient
12 |
13 |
14 | class AbstractBillHandler(Handler[Bill], abc.ABC):
15 | @property
16 | def wallet(self) -> QiwiP2PClient:
17 | return cast(QiwiP2PClient, self.context['wallet'])
18 |
19 | @property
20 | def bill_id(self) -> str:
21 | return self.event.id
22 |
23 | @property
24 | def bill_sum(self) -> PlainAmount:
25 | return self.event.amount
26 |
27 | @property
28 | def pay_url(self) -> str:
29 | return self.event.pay_url
30 |
31 | @property
32 | def shim_url(self) -> str:
33 | return self.wallet.create_shim_url(self.event.invoice_uid)
34 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/class_based/error.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | from typing import Any
5 |
6 | from .base import Handler
7 |
8 |
9 | class ErrorHandler(Handler[Exception], abc.ABC):
10 | def __init__(self, event: Exception, *args: Any) -> None:
11 | super().__init__(event)
12 | self.args = args
13 |
14 | @property
15 | def exception_name(self) -> str:
16 | return self.event.__class__.__qualname__
17 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/class_based/transaction.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | from typing import TYPE_CHECKING, cast
5 |
6 | from glQiwiApi.qiwi.clients.wallet.types.transaction import Transaction
7 | from glQiwiApi.types.amount import AmountWithCurrency
8 |
9 | from .base import Handler
10 |
11 | if TYPE_CHECKING:
12 | from glQiwiApi.qiwi.clients.wallet.client import QiwiWallet
13 |
14 |
15 | class AbstractTransactionHandler(Handler[Transaction], abc.ABC):
16 | @property
17 | def wallet(self) -> QiwiWallet:
18 | return cast(QiwiWallet, self.context['wallet'])
19 |
20 | @property
21 | def transaction_id(self) -> int:
22 | return self.event.id
23 |
24 | @property
25 | def transaction_sum(self) -> AmountWithCurrency:
26 | return self.event.sum
27 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/class_based/webhook_transaction.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | from typing import Optional
5 |
6 | from glQiwiApi.qiwi.clients.wallet.types.webhooks import TransactionWebhook, WebhookPayment
7 |
8 | from .base import Handler
9 |
10 |
11 | class AbstractTransactionWebhookHandler(Handler[TransactionWebhook], abc.ABC):
12 | @property
13 | def hook_id(self) -> str:
14 | return self.event.id
15 |
16 | @property
17 | def payment(self) -> Optional[WebhookPayment]:
18 | return self.event.payment
19 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/filters.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | import inspect
5 | from typing import Any, Awaitable, Callable, Generic, Tuple, Type, TypeVar, Union, cast
6 |
7 | Event = TypeVar('Event')
8 |
9 |
10 | class BaseFilter(abc.ABC, Generic[Event]):
11 | @abc.abstractmethod
12 | async def check(self, update: Event) -> bool:
13 | raise NotImplementedError
14 |
15 | def __and__(self, other: BaseFilter[Event]) -> AndFilter[Event]:
16 | if not isinstance(other, BaseFilter):
17 | raise TypeError(
18 | f"Can't compose two different types of filters, expected Filter, got {type(other)}"
19 | )
20 | return AndFilter(self, other)
21 |
22 | def __invert__(self) -> NotFilter[Event]:
23 | return NotFilter(self)
24 |
25 |
26 | class AndFilter(BaseFilter[Event]):
27 | def __init__(self, filter1: BaseFilter[Event], filter2: BaseFilter[Event]) -> None:
28 | self.filter2 = filter2
29 | self.filter1 = filter1
30 |
31 | async def check(self, update: Any) -> bool:
32 | return await self.filter1.check(update) and await self.filter2.check(update)
33 |
34 |
35 | class NotFilter(BaseFilter[Event]):
36 | def __init__(self, filter_: BaseFilter[Event]):
37 | self.filter_ = filter_
38 |
39 | async def check(self, update: Event) -> bool:
40 | return not await self.filter_.check(update)
41 |
42 |
43 | class LambdaBasedFilter(BaseFilter[Event]):
44 | def __init__(self, func: Callable[[Event], Union[bool, Awaitable[bool]]]) -> None:
45 | self.name = f'Filter around <{func!r}>'
46 |
47 | self.function = func
48 | self.awaitable: bool = inspect.iscoroutinefunction(func) or inspect.isawaitable(func)
49 |
50 | async def check(self, update: Event) -> bool:
51 | if self.awaitable:
52 | return await cast(Awaitable[bool], self.function(update))
53 | else:
54 | return cast(bool, self.function(update))
55 |
56 |
57 | class ExceptionFilter(BaseFilter[Event]):
58 | def __init__(self, exception: Union[Type[Exception], Tuple[Type[Exception]]]):
59 | self._exception = exception
60 |
61 | async def check(self, update: Event) -> bool:
62 | return isinstance(update, self._exception)
63 |
64 |
65 | __all__ = ('LambdaBasedFilter', 'BaseFilter', 'NotFilter', 'AndFilter', 'Event', 'ExceptionFilter')
66 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/__init__.py:
--------------------------------------------------------------------------------
1 | from glQiwiApi.core.event_fetching.webhooks.views.base import BaseWebhookView
2 |
3 | from . import app # noqa
4 | from .views import QiwiBillWebhookView, QiwiTransactionWebhookView
5 |
6 | __all__ = ('QiwiBillWebhookView', 'QiwiTransactionWebhookView', 'BaseWebhookView', 'app.py')
7 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/app.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 |
3 | from aiohttp import web
4 |
5 | from glQiwiApi.core.event_fetching.dispatcher import BaseDispatcher
6 | from glQiwiApi.core.event_fetching.webhooks.config import WebhookConfig
7 | from glQiwiApi.core.event_fetching.webhooks.middlewares.ip import ip_filter_middleware
8 | from glQiwiApi.core.event_fetching.webhooks.services.collision_detector import (
9 | HashBasedCollisionDetector,
10 | )
11 | from glQiwiApi.core.event_fetching.webhooks.services.security.ip import IPFilter
12 | from glQiwiApi.core.event_fetching.webhooks.utils import inject_dependencies
13 | from glQiwiApi.core.event_fetching.webhooks.views.bill_view import QiwiBillWebhookView
14 | from glQiwiApi.core.event_fetching.webhooks.views.transaction_view import (
15 | QiwiTransactionWebhookView,
16 | )
17 | from glQiwiApi.qiwi.clients.p2p.types import BillWebhook
18 | from glQiwiApi.qiwi.clients.wallet.types.webhooks import TransactionWebhook
19 |
20 |
21 | def configure_app(
22 | dispatcher: BaseDispatcher, app: web.Application, webhook_config: WebhookConfig
23 | ) -> web.Application:
24 | """
25 | Entirely configures the web app for webhooks.
26 |
27 | :param dispatcher: dispatcher, which processing events
28 | :param app: aiohttp.web.Application
29 | :param webhook_config:
30 | """
31 |
32 | generic_dependencies: t.Dict[str, t.Any] = {
33 | 'dispatcher': dispatcher,
34 | 'collision_detector': HashBasedCollisionDetector(),
35 | }
36 |
37 | app.router.add_view(
38 | handler=inject_dependencies(
39 | QiwiBillWebhookView,
40 | {
41 | 'event_cls': BillWebhook,
42 | 'encryption_key': webhook_config.encryption.secret_p2p_key,
43 | **generic_dependencies,
44 | },
45 | ),
46 | name=webhook_config.routes.p2p_view_route_name,
47 | path=webhook_config.routes.p2p_path,
48 | )
49 |
50 | app.router.add_view(
51 | handler=inject_dependencies(
52 | QiwiTransactionWebhookView,
53 | {
54 | 'event_cls': TransactionWebhook,
55 | 'encryption_key': webhook_config.encryption.base64_encryption_key,
56 | **generic_dependencies,
57 | },
58 | ),
59 | name=webhook_config.routes.standard_qiwi_route_name,
60 | path=webhook_config.routes.standard_qiwi_hook_path,
61 | )
62 |
63 | if webhook_config.security.check_ip:
64 | app.middlewares.append(ip_filter_middleware(IPFilter.default()))
65 |
66 | return app
67 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/config.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import ssl
4 | from dataclasses import dataclass, field
5 | from typing import Any, Dict, Optional
6 |
7 | from aiohttp import web
8 |
9 | from glQiwiApi.utils.certificates import SSLCertificate
10 |
11 | DEFAULT_QIWI_WEBHOOK_PATH = '/webhooks/qiwi/transactions/'
12 | DEFAULT_QIWI_ROUTE_NAME = 'QIWI_TRANSACTIONS'
13 |
14 | DEFAULT_QIWI_BILLS_WEBHOOK_PATH = '/webhooks/qiwi/bills/'
15 | DEFAULT_QIWI_BILLS_ROUTE_NAME = 'QIWI_BILLS'
16 |
17 |
18 | @dataclass()
19 | class ApplicationConfig:
20 | base_app: Optional[web.Application] = None
21 |
22 | host: str = 'localhost'
23 | 'server host'
24 |
25 | port: int = 8080
26 | 'server port that open for tcp/ip trans.'
27 |
28 | ssl_certificate: Optional[SSLCertificate] = None
29 |
30 | kwargs: Dict[Any, Any] = field(default_factory=dict)
31 |
32 | @property
33 | def ssl_context(self) -> Optional[ssl.SSLContext]:
34 | if self.ssl_certificate is None:
35 | return None
36 | return self.ssl_certificate.as_ssl_context()
37 |
38 |
39 | @dataclass()
40 | class RoutesConfig:
41 | p2p_path: str = DEFAULT_QIWI_BILLS_WEBHOOK_PATH
42 | standard_qiwi_hook_path: str = DEFAULT_QIWI_WEBHOOK_PATH
43 |
44 | p2p_view_route_name: str = DEFAULT_QIWI_BILLS_ROUTE_NAME
45 | standard_qiwi_route_name: str = DEFAULT_QIWI_ROUTE_NAME
46 |
47 |
48 | @dataclass()
49 | class EncryptionConfig:
50 | secret_p2p_key: str
51 | base64_encryption_key: Optional[
52 | str
53 | ] = None # taken from QIWI API using QiwiWallet instance by default
54 |
55 |
56 | @dataclass()
57 | class SecurityConfig:
58 | check_ip: bool = True
59 |
60 |
61 | @dataclass()
62 | class HookRegistrationConfig:
63 | host_or_ip_address: Optional[str] = None
64 |
65 |
66 | @dataclass
67 | class WebhookConfig:
68 | encryption: EncryptionConfig
69 | hook_registration: HookRegistrationConfig = field(default_factory=HookRegistrationConfig)
70 | app: ApplicationConfig = field(default_factory=ApplicationConfig)
71 | routes: RoutesConfig = field(default_factory=RoutesConfig)
72 | security: SecurityConfig = field(default_factory=SecurityConfig)
73 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/dto/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/core/event_fetching/webhooks/dto/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/dto/errors.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class WebhookAPIError(BaseModel):
7 | status: str
8 | detail: Optional[str] = None
9 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/middlewares/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/core/event_fetching/webhooks/middlewares/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/middlewares/ip.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Any, Awaitable, Callable
3 |
4 | from aiohttp import web
5 | from aiohttp.typedefs import Handler
6 | from aiohttp.web_middlewares import middleware
7 |
8 | from glQiwiApi.core.event_fetching.webhooks.services.security.ip import IPFilter
9 | from glQiwiApi.core.event_fetching.webhooks.utils import check_ip
10 |
11 | logger = logging.getLogger('glQiwiApi.webhooks.middlewares')
12 |
13 |
14 | def ip_filter_middleware(ip_filter: IPFilter) -> Callable[[web.Request, Handler], Awaitable[Any]]:
15 | @middleware
16 | async def _ip_filter_middleware(request: web.Request, handler: Handler) -> Any:
17 | ip_address, accept = check_ip(ip_filter=ip_filter, request=request)
18 | if not accept:
19 | logger.warning(f'Blocking request from an unauthorized IP: {ip_address}')
20 | raise web.HTTPUnauthorized()
21 | return await handler(request)
22 |
23 | return _ip_filter_middleware
24 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/core/event_fetching/webhooks/services/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/services/collision_detector.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from typing import Any, Generic, Set, TypeVar
3 |
4 | T = TypeVar('T')
5 |
6 |
7 | class UnexpectedCollision(Exception):
8 | pass
9 |
10 |
11 | class UnhashableObjectError(TypeError):
12 | pass
13 |
14 |
15 | class AbstractCollisionDetector(abc.ABC, Generic[T]):
16 | """
17 | QIWI API can transfer_money the same update twice or more, so we need to avoid this problem anyway.
18 | Also, you can override it with redis usage or more advanced hashing.
19 | """
20 |
21 | @abc.abstractmethod
22 | def has_collision(self, obj: T) -> bool:
23 | raise NotImplementedError
24 |
25 | @abc.abstractmethod
26 | def add_already_processed_event(self, obj: T) -> None:
27 | raise NotImplementedError
28 |
29 | def remember_processed_object(self, obj: T) -> None:
30 | if self.has_collision(obj):
31 | raise UnexpectedCollision()
32 | self.add_already_processed_event(obj)
33 |
34 |
35 | class HashBasedCollisionDetector(AbstractCollisionDetector[T]):
36 | def __init__(self) -> None:
37 | self.already_processed_object_hashes: Set[int] = set()
38 |
39 | def add_already_processed_event(self, obj: T) -> None:
40 | if _is_object_unhashable(obj):
41 | raise UnhashableObjectError(f'Object {obj!r} is unhashable')
42 | self.already_processed_object_hashes.add(hash(obj))
43 |
44 | def has_collision(self, obj: T) -> bool:
45 | if _is_object_unhashable(obj):
46 | raise UnhashableObjectError(f'Object {obj!r} is unhashable')
47 | return any(
48 | hash(obj) == processed_hash for processed_hash in self.already_processed_object_hashes
49 | )
50 |
51 |
52 | def _is_object_unhashable(obj: Any) -> bool:
53 | try:
54 | hash(obj)
55 | return False
56 | except TypeError:
57 | return True
58 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/services/security/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/core/event_fetching/webhooks/services/security/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/services/security/ip.py:
--------------------------------------------------------------------------------
1 | from ipaddress import IPv4Address, IPv4Network
2 | from typing import Optional, Sequence, Set, Union
3 |
4 | DEFAULT_QIWI_NETWORKS = [
5 | IPv4Network('79.142.16.0/20'),
6 | IPv4Network('195.189.100.0/22'),
7 | IPv4Network('91.232.230.0/23'),
8 | IPv4Network('91.213.51.0/24'),
9 | ]
10 |
11 |
12 | class IPFilter:
13 | def __init__(self, ips: Optional[Sequence[Union[str, IPv4Network, IPv4Address]]] = None):
14 | self._allowed_ips: Set[IPv4Address] = set()
15 |
16 | if ips:
17 | self.allow(*ips)
18 |
19 | def allow(self, *ips: Union[str, IPv4Network, IPv4Address]) -> None:
20 | for ip in ips:
21 | self.allow_ip(ip)
22 |
23 | def allow_ip(self, ip: Union[str, IPv4Network, IPv4Address]) -> None:
24 | if isinstance(ip, str):
25 | ip = IPv4Network(ip) if '/' in ip else IPv4Address(ip)
26 | if isinstance(ip, IPv4Address):
27 | self._allowed_ips.add(ip)
28 | elif isinstance(ip, IPv4Network):
29 | self._allowed_ips.update(ip.hosts())
30 | else:
31 | raise ValueError(f"Invalid type of ipaddress: {type(ip)} ('{ip}')")
32 |
33 | @classmethod
34 | def default(cls) -> 'IPFilter':
35 | return cls(DEFAULT_QIWI_NETWORKS)
36 |
37 | def __contains__(self, item: Union[str, IPv4Address]) -> bool:
38 | return self.check(item)
39 |
40 | def check(self, ip: Union[str, IPv4Address]) -> bool:
41 | if not isinstance(ip, IPv4Address):
42 | ip = IPv4Address(ip)
43 | return ip in self._allowed_ips
44 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import inspect
3 | import sys
4 | from asyncio import Transport
5 | from typing import Any, Mapping, Tuple, Type, TypeVar, cast, no_type_check
6 |
7 | from aiohttp import web
8 | from aiohttp.abc import AbstractView
9 | from aiohttp.web_request import Request
10 |
11 | from glQiwiApi.core.event_fetching.webhooks.services.security.ip import IPFilter
12 |
13 |
14 | def check_ip(ip_filter: IPFilter, request: web.Request) -> Tuple[str, bool]:
15 | # Try to resolve client IP over reverse proxy
16 | forwarded_for = request.headers.get('X-Forwarded-For', '')
17 | if forwarded_for:
18 | forwarded_for, *_ = forwarded_for.split(',', maxsplit=1)
19 | return forwarded_for, forwarded_for in ip_filter
20 |
21 | peer_name = cast(Transport, request.transport).get_extra_info('peername')
22 | # When reverse proxy is not configured IP address can be resolved from incoming connection
23 | if peer_name:
24 | host, _ = peer_name
25 | return host, host in ip_filter
26 |
27 | # Potentially impossible case
28 | return '', False # pragma: no cover
29 |
30 |
31 | View = TypeVar('View', bound=Type[AbstractView])
32 |
33 |
34 | def inject_dependencies(view: View, dependencies: Mapping[str, Any]) -> View:
35 | params = inspect.signature(view.__init__).parameters
36 |
37 | deps = {
38 | name: dependency
39 | for name, dependency in dependencies.items()
40 | if not isinstance(dependency, (Request, AbstractView)) and name in params
41 | }
42 |
43 | return cast(View, partial_class(view.__qualname__, view, **deps)) # type: ignore
44 |
45 |
46 | @no_type_check
47 | def partial_class(name, cls, *args, **kwds):
48 | new_cls = type(
49 | name, (cls,), {'__init__': functools.partialmethod(cls.__init__, *args, **kwds)}
50 | )
51 |
52 | # The following is copied nearly ad verbatim from `namedtuple's` source.
53 | """
54 | # For pickling to work, the __module__ variable needs to be set to the frame
55 | # where the named tuple is created. Bypass this step in enviroments where
56 | # sys._getframe is not defined (Jython for example) or sys._getframe is not
57 | # defined for arguments greater than 0 (IronPython).
58 | """
59 | try:
60 | new_cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__') # noqa
61 | except (AttributeError, ValueError): # pragma: no cover
62 | pass
63 |
64 | return new_cls
65 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .bill_view import QiwiBillWebhookView
2 | from .transaction_view import QiwiTransactionWebhookView
3 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/views/base.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import logging
3 | from typing import TYPE_CHECKING as MYPY
4 | from typing import Any, Generic, Type, TypeVar
5 |
6 | from aiohttp import web
7 | from aiohttp.web_request import Request
8 |
9 | from glQiwiApi.core.event_fetching.dispatcher import BaseDispatcher
10 | from glQiwiApi.core.event_fetching.webhooks.dto import WebhookAPIError
11 | from glQiwiApi.core.event_fetching.webhooks.services.collision_detector import (
12 | AbstractCollisionDetector,
13 | UnexpectedCollision,
14 | )
15 | from glQiwiApi.utils.compat import json
16 |
17 | if MYPY:
18 | from glQiwiApi.types.base import HashableBase # noqa
19 |
20 | Event = TypeVar('Event', bound='HashableBase')
21 |
22 | logger = logging.getLogger('glQiwiApi.webhooks.base')
23 |
24 |
25 | class BaseWebhookView(web.View, Generic[Event]):
26 | """
27 | Generic webhook view for processing events
28 | You can make your own view and than use it in code, just inheriting this base class
29 |
30 | """
31 |
32 | def __init__(
33 | self,
34 | request: Request,
35 | dispatcher: BaseDispatcher,
36 | collision_detector: AbstractCollisionDetector[Any],
37 | event_cls: Type[Event],
38 | encryption_key: str,
39 | ) -> None:
40 | super().__init__(request)
41 | self._dispatcher = dispatcher
42 | self._collision_detector = collision_detector
43 | self._event_cls = event_cls
44 | self._encryption_key = encryption_key
45 |
46 | @abc.abstractmethod
47 | async def ok_response(self) -> web.Response:
48 | pass
49 |
50 | async def get(self) -> web.Response:
51 | return web.Response(text='')
52 |
53 | async def post(self) -> web.Response:
54 | event = await self._parse_raw_request()
55 |
56 | try:
57 | self._collision_detector.remember_processed_object(event)
58 | except UnexpectedCollision:
59 | logger.debug('Detect collision on event %s', event)
60 | return await self.ok_response()
61 |
62 | self._validate_event_signature(event)
63 | await self.process_event(event)
64 | return await self.ok_response()
65 |
66 | async def _parse_raw_request(self) -> Event:
67 | """Parse raw update and return pydantic model"""
68 | try:
69 | data = await self.request.json(loads=json.loads)
70 | if isinstance(data, str):
71 | return self._event_cls.parse_raw(data)
72 | elif isinstance(data, dict): # pragma: no cover
73 | return self._event_cls.parse_obj(data)
74 | else:
75 | raise ValueError()
76 | except ValueError:
77 | raise web.HTTPBadRequest(
78 | text=WebhookAPIError(status='Validation error').json(),
79 | content_type='application/json',
80 | )
81 |
82 | @abc.abstractmethod
83 | def _validate_event_signature(self, update: Event) -> None:
84 | raise NotImplementedError
85 |
86 | async def process_event(self, event: Event) -> None:
87 | await self._dispatcher.process_event(event)
88 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/views/bill_view.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import cast
3 |
4 | from aiohttp import web
5 |
6 | from glQiwiApi.core.event_fetching.webhooks.views.base import BaseWebhookView
7 | from glQiwiApi.qiwi.clients.p2p.types import BillWebhook
8 | from glQiwiApi.types.exceptions import WebhookSignatureUnverifiedError
9 |
10 | logger = logging.getLogger('glQiwiApi.webhooks.p2p')
11 |
12 |
13 | class QiwiBillWebhookView(BaseWebhookView[BillWebhook]):
14 | """View, which processes p2p notifications"""
15 |
16 | def _validate_event_signature(self, update: BillWebhook) -> None:
17 | sha256_signature = cast(
18 | str, self.request.headers.get('X-Api-Signature-SHA256')
19 | ) # pragma: no cover
20 |
21 | try: # pragma: no cover
22 | update.verify_signature(sha256_signature, self._encryption_key) # pragma: no cover
23 | except WebhookSignatureUnverifiedError: # pragma: no cover
24 | logger.debug(
25 | 'Blocking request due to invalid signature of payload.'
26 | ) # pragma: no cover
27 | raise web.HTTPBadRequest() # pragma: no cover
28 |
29 | async def ok_response(self) -> web.Response:
30 | return web.json_response(data={'error': '0'})
31 |
--------------------------------------------------------------------------------
/glQiwiApi/core/event_fetching/webhooks/views/transaction_view.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from aiohttp import web
4 |
5 | from glQiwiApi.core.event_fetching.webhooks.dto import WebhookAPIError
6 | from glQiwiApi.core.event_fetching.webhooks.views.base import BaseWebhookView
7 | from glQiwiApi.qiwi.clients.wallet.types.webhooks import TransactionWebhook
8 | from glQiwiApi.types.exceptions import WebhookSignatureUnverifiedError
9 |
10 | logger = logging.getLogger('glQiwiApi.webhooks.transaction')
11 |
12 |
13 | class QiwiTransactionWebhookView(BaseWebhookView[TransactionWebhook]):
14 | def _validate_event_signature(self, update: TransactionWebhook) -> None:
15 | if update.is_experimental: # pragma: no cover
16 | return None
17 |
18 | logger.debug('Current encryption key is %s', self._encryption_key)
19 |
20 | try:
21 | update.verify_signature(self._encryption_key)
22 | except WebhookSignatureUnverifiedError:
23 | logger.debug(
24 | 'Request has being blocked due to invalid signature of json request payload.'
25 | )
26 | raise web.HTTPBadRequest(
27 | text=WebhookAPIError(status='Invalid hash of transaction.').json(),
28 | content_type='application/json',
29 | )
30 |
31 | async def ok_response(self) -> web.Response:
32 | return web.Response(text='ok')
33 |
--------------------------------------------------------------------------------
/glQiwiApi/core/session/__init__.py:
--------------------------------------------------------------------------------
1 | from .holder import AbstractSessionHolder, AiohttpSessionHolder
2 |
3 | __all__ = ('AbstractSessionHolder', 'AiohttpSessionHolder')
4 |
--------------------------------------------------------------------------------
/glQiwiApi/ext/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/ext/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/ext/webhook_url.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import re
4 | from collections import namedtuple
5 | from typing import Any, Optional, Pattern, Type, TypeVar, cast
6 |
7 | from glQiwiApi.core.event_fetching.webhooks.config import DEFAULT_QIWI_WEBHOOK_PATH
8 |
9 | _URL = TypeVar('_URL', bound='WebhookURL')
10 |
11 | # Url without port e.g. https://127.0.0.1/ or https://website.com/
12 | HOST_REGEX: Pattern[str] = re.compile(
13 | r'^(http(s?)://)?'
14 | r'(((www\.)?[a-zA-Z0-9.\-_]+'
15 | r'(\.[a-zA-Z]{2,6})+)|'
16 | r'(\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.)'
17 | r'{3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b))'
18 | r'(/[a-zA-Z0-9_\-\s./?%#&=]*)?$'
19 | )
20 |
21 |
22 | class WebhookURL(
23 | namedtuple(
24 | 'WebhookURL',
25 | ['host', 'webhook_path', 'port', 'https'],
26 | )
27 | ):
28 | host: str
29 | webhook_path: Optional[str]
30 | port: Optional[int]
31 | https: bool = False
32 |
33 | @classmethod
34 | def create(
35 | cls: Type[_URL],
36 | host: str,
37 | port: Optional[int] = None,
38 | webhook_path: Optional[str] = None,
39 | https: bool = False,
40 | ) -> _URL:
41 | return cls(
42 | host=cls._assert_host(host, param_name='host'),
43 | webhook_path=cls._assert_str(webhook_path, param_name='webhook_path'),
44 | port=cls._assert_int(port, param_name='port'),
45 | https=https,
46 | )
47 |
48 | @classmethod
49 | def _assert_int(cls, v: Optional[Any], *, param_name: str) -> Optional[int]:
50 | if v is None:
51 | return v
52 |
53 | if not isinstance(v, int):
54 | raise TypeError('%s must be integer' % param_name)
55 | return v
56 |
57 | @classmethod
58 | def _assert_str(cls, v: Optional[Any], *, param_name: str) -> Optional[str]:
59 | if v is None:
60 | return v
61 |
62 | if not isinstance(v, str):
63 | raise TypeError('%s must be a string' % param_name)
64 | return v
65 |
66 | @classmethod
67 | def _assert_host(cls, v: Any, *, param_name: str) -> str:
68 | if not re.match(HOST_REGEX, v):
69 | raise TypeError(
70 | '%s must be like https://127.0.0.1/ or https://website.com/' % param_name
71 | )
72 | return cast(str, v)
73 |
74 | def render(self) -> str:
75 | host = self.host
76 | if self.webhook_path is None:
77 | # Here we use `DEFAULT_QIWI_WEBHOOK_PATH` instead of DEFAULT_QIWI_BILLS_WEBHOOK_PATH
78 | # because the second you need to register directly in QIWI P2P API and it's no need to build endpoint to it
79 | self.webhook_path = DEFAULT_QIWI_WEBHOOK_PATH
80 | if self.port is not None:
81 | host += f':{self.port}'
82 | if self._doesnt_contains_slash():
83 | host += '/'
84 | if self.https:
85 | scheme = 'https://'
86 | else:
87 | scheme = 'http://'
88 | return f'{scheme}{host}{self.webhook_path}'
89 |
90 | def _doesnt_contains_slash(self) -> bool:
91 | return not (self.host.endswith('/') and self.webhook_path.startswith('/')) # type: ignore
92 |
--------------------------------------------------------------------------------
/glQiwiApi/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/py.typed
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/base.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import types
3 | from http import HTTPStatus
4 | from json import JSONDecodeError
5 | from typing import Any, ClassVar, Generic, Sequence, TypeVar, cast
6 |
7 | from glQiwiApi.core.abc.api_method import APIMethod, ReturningType, _sentinel
8 | from glQiwiApi.core.session.holder import HTTPResponse
9 | from glQiwiApi.qiwi.exceptions import QiwiAPIError
10 |
11 | try:
12 | from orjson import JSONDecodeError as OrjsonDecodeError
13 | except ImportError:
14 | from json import JSONDecodeError as OrjsonDecodeError
15 |
16 | T = TypeVar('T', bound=Any)
17 |
18 |
19 | class QiwiAPIMethod(APIMethod[T], abc.ABC, Generic[T]):
20 | arbitrary_allowed_response_status_codes: ClassVar[Sequence[int]] = ()
21 |
22 | @classmethod
23 | def parse_http_response(cls, response: HTTPResponse) -> ReturningType:
24 | response_is_successful = cls.check_if_response_status_success(response)
25 |
26 | try:
27 | json_response = response.json()
28 | except (JSONDecodeError, TypeError, OrjsonDecodeError):
29 | response_is_successful = False
30 |
31 | if not response_is_successful:
32 | QiwiAPIError(response).raise_exception_matching_error_code()
33 |
34 | # micro optimization that helps to avoid json re-deserialization
35 | response.json = types.MethodType(lambda self: json_response, response) # type: ignore
36 |
37 | manually_parsed_json = cls.on_json_parse(response)
38 | if manually_parsed_json is not _sentinel:
39 | return cast(ReturningType, manually_parsed_json)
40 |
41 | return super().parse_http_response(response)
42 |
43 | @classmethod
44 | def check_if_response_status_success(cls, response: HTTPResponse) -> bool:
45 | if response.status_code == HTTPStatus.OK:
46 | return True
47 | elif response.status_code in cls.arbitrary_allowed_response_status_codes:
48 | return True
49 | return False
50 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/maps/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/methods/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/maps/methods/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/methods/get_partners.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 | from glQiwiApi.qiwi.clients.wallet.types import Partner
5 |
6 |
7 | class GetPartners(QiwiAPIMethod[List[Partner]]):
8 | url: ClassVar[str] = 'http://edge.qiwi.com/locator/v3/ttp-groups'
9 | http_method: ClassVar[str] = 'GET'
10 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/methods/get_terminals.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, List, Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import Request
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.maps.types.polygon import Polygon
8 | from glQiwiApi.qiwi.clients.maps.types.terminal import Terminal
9 |
10 |
11 | class GetTerminals(QiwiAPIMethod[List[Terminal]]):
12 | url: ClassVar[str] = 'http://edge.qiwi.com/locator/v3/nearest/clusters?parameters'
13 | http_method: ClassVar[str] = 'GET'
14 |
15 | polygon: Polygon
16 | pop_if_inactive_x_mins: int = Field(..., alias='activeWithinMinutes')
17 | zoom: Optional[int] = None
18 | include_partners: Optional[bool] = Field(None, alias='withRefillWallet')
19 | partners_ids: Optional[List[Any]] = Field(None, alias='ttpIds')
20 | cache_terminals: Optional[bool] = Field(None, alias='cacheAllowed')
21 | card_terminals: Optional[bool] = Field(None, alias='cardAllowed')
22 | identification_types: Optional[int] = Field(None, alias='identificationTypes')
23 | terminal_groups: Optional[List[Any]] = Field(
24 | None,
25 | alias='ttpGroups',
26 | )
27 |
28 | def build_request(self, **url_format_kw: Any) -> 'Request':
29 | model_dict = self.dict(exclude_none=True, exclude_unset=True, by_alias=True)
30 | polygon = model_dict.pop('polygon')
31 |
32 | model_dict = _replace_bool_values_with_strings(model_dict)
33 |
34 | return Request(
35 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
36 | http_method=self.http_method,
37 | params={**model_dict, **polygon},
38 | )
39 |
40 |
41 | def _replace_bool_values_with_strings(d: Dict[str, Any]) -> Dict[str, Any]:
42 | for k, v in d.items():
43 | if not isinstance(v, bool):
44 | continue
45 |
46 | d[k] = str(v)
47 | return d
48 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/types/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/maps/types/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/types/polygon.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 |
4 | class Polygon(BaseModel):
5 | """Polygon class for QiwiMaps class"""
6 |
7 | latitude_north_western: float = Field(..., alias='latNW')
8 | longitude_north_western: float = Field(..., alias='lngNW')
9 | latitude_south_east: float = Field(..., alias='latSE')
10 | longitude_south_east: float = Field(..., alias='lngSE')
11 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/maps/types/terminal.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.types.base import Base
6 |
7 |
8 | class Coordinate(Base):
9 | """Object: coordinate"""
10 |
11 | latitude: float = Field(..., alias='latitude')
12 | longitude: float = Field(..., alias='longitude')
13 | precision: int = Field(..., alias='precision')
14 |
15 |
16 | class Terminal(Base):
17 | """Object: Terminal"""
18 |
19 | terminal_id: int = Field(..., alias='terminalId')
20 | ttp_id: int = Field(..., alias='ttpId')
21 | last_active: str = Field(..., alias='lastActive')
22 | count: int = Field(..., alias='count')
23 | address: str = Field(..., alias='address')
24 | verified: bool = Field(..., alias='verified')
25 | label: str = Field(..., alias='label')
26 | description: Optional[str] = Field(type(None), alias='description')
27 | cash_allowed: bool = Field(..., alias='cashAllowed')
28 | card_allowed: bool = Field(..., alias='cardAllowed')
29 | identification_type: int = Field(..., alias='identificationType')
30 | coordinate: Coordinate = Field(..., alias='coordinate')
31 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/p2p/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/methods/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/p2p/methods/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/methods/create_p2p_bill.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from datetime import datetime, timedelta
3 | from typing import Any, ClassVar, Dict, List, Optional, Union
4 |
5 | from pydantic import Field, validator
6 |
7 | from glQiwiApi.core.abc.api_method import Request, RuntimeValue
8 | from glQiwiApi.qiwi.base import QiwiAPIMethod
9 | from glQiwiApi.qiwi.clients.p2p.types import Bill, Customer
10 | from glQiwiApi.utils.date_conversion import datetime_to_iso8601_with_moscow_timezone
11 |
12 |
13 | def get_default_bill_life_time() -> datetime:
14 | return datetime.now() + timedelta(days=2)
15 |
16 |
17 | class CreateP2PBill(QiwiAPIMethod[Bill]):
18 | http_method: ClassVar[str] = 'PUT'
19 | url: ClassVar[str] = 'https://api.qiwi.com/partner/bill/v1/bills/{bill_id}'
20 |
21 | json_payload_schema: ClassVar[Dict[str, Any]] = {
22 | 'amount': {'currency': 'RUB', 'value': RuntimeValue()},
23 | 'expirationDateTime': RuntimeValue(default_factory=get_default_bill_life_time),
24 | 'comment': RuntimeValue(mandatory=False),
25 | 'customFields': {
26 | 'paySourcesFilter': RuntimeValue(default=['qw', 'card']),
27 | 'themeCode': RuntimeValue(default='Yvan-YKaSh'),
28 | },
29 | 'customer': RuntimeValue(mandatory=False),
30 | }
31 |
32 | bill_id: Optional[str] = Field(None, path_runtime_value=True)
33 | expire_at: Optional[datetime] = Field(
34 | scheme_path='expirationDateTime',
35 | )
36 | amount: float = Field(..., scheme_path='amount.value')
37 | comment: Optional[str] = Field(None, scheme_path='comment')
38 | theme_code: Optional[str] = Field(None, scheme_path='customFields.themeCode')
39 | pay_source_filter: Optional[List[str]] = Field(
40 | None, scheme_path='customFields.paySourcesFilter'
41 | )
42 | customer: Optional[Customer] = Field(None, schema_path='customer')
43 |
44 | @validator('bill_id')
45 | def set_bill_id_if_it_is_none(cls, v: Optional[str]) -> str:
46 | return v or str(uuid.uuid4())
47 |
48 | def build_request(self, **url_format_kw: Any) -> 'Request':
49 | request = super().build_request(**url_format_kw)
50 | expire_at = request.json_payload['expirationDateTime'] # type: ignore
51 | request.json_payload['expirationDateTime'] = datetime_to_iso8601_with_moscow_timezone(
52 | expire_at
53 | )
54 | pay_source_filter = request.json_payload['customFields']['paySourcesFilter']
55 | if isinstance(pay_source_filter, list):
56 | request.json_payload['customFields']['paySourcesFilter'] = ' '.join(
57 | pay_source_filter
58 | ).replace(' ', ',')
59 | return request
60 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/methods/create_p2p_key_pair.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import RuntimeValue
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.p2p.types import PairOfP2PKeys
8 |
9 |
10 | class CreateP2PKeyPair(QiwiAPIMethod[PairOfP2PKeys]):
11 | http_method: ClassVar[str] = 'POST'
12 | url: ClassVar[
13 | str
14 | ] = 'https://api.qiwi.com/partner/bill/v1/bills/widgets-api/api/p2p/protected/keys/create'
15 |
16 | json_payload_schema: ClassVar[Dict[str, Any]] = {
17 | 'keysPairName': RuntimeValue(),
18 | 'serverNotificationsUrl': RuntimeValue(),
19 | }
20 |
21 | key_pair_name: str = Field(..., scheme_path='keysPairName')
22 | server_notification_url: Optional[str] = Field(..., scheme_path='serverNotificationsUrl')
23 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/methods/get_bill_by_id.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 | from glQiwiApi.qiwi.clients.p2p.types import Bill
7 |
8 |
9 | class GetBillByID(QiwiAPIMethod[Bill]):
10 | http_method: ClassVar[str] = 'GET'
11 | url: ClassVar[str] = 'https://api.qiwi.com/partner/bill/v1/bills/{bill_id}'
12 |
13 | bill_id: str = Field(..., path_runtime_value=True)
14 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/methods/refund_bill.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, Union
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import Request
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.p2p.types import RefundedBill
8 | from glQiwiApi.types.amount import PlainAmount
9 |
10 |
11 | class RefundBill(QiwiAPIMethod[RefundedBill]):
12 | http_method: ClassVar[str] = 'POST'
13 | url: ClassVar[str] = 'https://api.qiwi.com/partner/bill/v1/bills/{bill_id}/refunds/{refund_id}'
14 |
15 | bill_id: str = Field(..., path_runtime_value=True)
16 | refund_id: str = Field(..., path_runtime_value=True)
17 |
18 | json_bill_data: Union[PlainAmount, Dict[str, Union[str, int]]]
19 |
20 | def build_request(self, **url_format_kw: Any) -> 'Request':
21 | json_payload = self.json_bill_data
22 | if isinstance(self.json_bill_data, PlainAmount):
23 | json_payload = self.json_bill_data.json(encoder=self.Config.json_dumps) # type: ignore
24 |
25 | return Request(
26 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
27 | json_payload=json_payload,
28 | http_method=self.http_method,
29 | )
30 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/p2p/methods/reject_p2p_bill.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 | from glQiwiApi.qiwi.clients.p2p.types import Bill
7 |
8 |
9 | class RejectP2PBill(QiwiAPIMethod[Bill]):
10 | http_method: ClassVar[str] = 'POST'
11 | url: ClassVar[str] = 'https://api.qiwi.com/partner/bill/v1/bills/{bill_id}/reject'
12 |
13 | bill_id: str = Field(..., path_runtime_value=True)
14 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/wallet/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/wallet/methods/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/authenticate_wallet.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 |
7 |
8 | class AuthenticateWallet(QiwiAPIMethod[Dict[str, Any]]):
9 | url: ClassVar[
10 | str
11 | ] = 'https://edge.qiwi.com/identification/v1/persons/{phone_number}/identification'
12 | http_method: ClassVar[str] = 'POST'
13 |
14 | passport: str
15 | birth_date: str = Field(..., alias='birthDate')
16 | first_name: str = Field(..., alias='firstName')
17 | middle_name: str = Field(..., alias='middleName')
18 | last_name: str = Field(..., alias='lastName')
19 | inn: str = ''
20 | snils: str = ''
21 | oms: str = ''
22 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/check_restriction.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 | from glQiwiApi.qiwi.clients.wallet.types.restriction import Restriction
5 |
6 |
7 | class GetRestrictions(QiwiAPIMethod[List[Restriction]]):
8 | url: ClassVar[
9 | str
10 | ] = 'https://edge.qiwi.com/person-profile/v1/persons/{phone_number}/status/restrictions'
11 | http_method: ClassVar[str] = 'GET'
12 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/create_new_balance.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 | from typing import Any, ClassVar, Dict, Sequence
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.abc.api_method import Request
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 |
9 |
10 | class CreateNewBalance(QiwiAPIMethod[Dict[str, Any]]):
11 | url: ClassVar[str] = 'https://edge.qiwi.com/funding-sources/v2/persons/{phone_number}/accounts'
12 | http_method: ClassVar[str] = 'POST'
13 | arbitrary_allowed_response_status_codes: ClassVar[Sequence[int]] = [HTTPStatus.CREATED]
14 |
15 | currency_alias: str = Field(..., alias='alias')
16 |
17 | def build_request(self, **url_format_kw: Any) -> 'Request':
18 | return Request(
19 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
20 | http_method=self.http_method,
21 | json_payload={'alias': self.currency_alias},
22 | )
23 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/detect_mobile_number.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, cast
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import ReturningType
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types.mobile_operator import MobileOperator
9 | from glQiwiApi.qiwi.exceptions import MobileOperatorCannotBeDeterminedError
10 |
11 |
12 | class DetectMobileNumber(QiwiAPIMethod[MobileOperator]):
13 | url: ClassVar[str] = 'https://qiwi.com/mobile/detect.action'
14 | http_method: ClassVar[str] = 'POST'
15 |
16 | phone_number: str = Field(..., alias='phone')
17 |
18 | @classmethod
19 | def parse_http_response(cls, response: HTTPResponse) -> ReturningType:
20 | mobile_operator: MobileOperator = super().parse_http_response(response)
21 | if mobile_operator.code.value == '2' or mobile_operator.code.name == 'ERROR':
22 | raise MobileOperatorCannotBeDeterminedError(
23 | response, custom_message=mobile_operator.message
24 | )
25 |
26 | return cast(ReturningType, mobile_operator)
27 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/fetch_statistics.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 | from typing import Any, ClassVar, Dict, List, Optional
3 |
4 | from pydantic import Field, root_validator
5 |
6 | from glQiwiApi.core.abc.api_method import Request
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types import Statistic, TransactionType
9 | from glQiwiApi.utils.date_conversion import datetime_to_iso8601_with_moscow_timezone
10 |
11 |
12 | class FetchStatistics(QiwiAPIMethod[Statistic]):
13 | url: ClassVar[
14 | str
15 | ] = 'https://edge.qiwi.com/payment-history/v2/persons/{phone_number}/payments/total'
16 | http_method: ClassVar[str] = 'GET'
17 |
18 | start_date: datetime = Field(default_factory=datetime.now)
19 | end_date: datetime = Field(default_factory=lambda: datetime.now() - timedelta(days=90))
20 | operation: TransactionType = TransactionType.ALL
21 | sources: Optional[List[str]] = None
22 |
23 | @root_validator()
24 | def check_start_date_and_end_date_difference_not_greater_than_90_days(
25 | cls, values: Dict[str, Any]
26 | ) -> Dict[str, Any]:
27 | start_date: Optional[datetime] = values.get('start_date')
28 | end_date: Optional[datetime] = values.get('end_date')
29 |
30 | if start_date is None or end_date is None: # denotes that type of arguments is invalid
31 | return values
32 |
33 | if (end_date - start_date).days > 90 or (start_date - end_date).days > 90:
34 | raise ValueError('The maximum period for downloading statistics is 90 calendar days.')
35 |
36 | return values
37 |
38 | def build_request(self, **url_format_kw: Any) -> Request:
39 | params = {
40 | 'startDate': datetime_to_iso8601_with_moscow_timezone(self.start_date),
41 | 'endDate': datetime_to_iso8601_with_moscow_timezone(self.end_date),
42 | 'operation': self.operation.value,
43 | }
44 | if self.sources:
45 | params.update({'sources': ' '.join(self.sources)})
46 | return Request(
47 | endpoint=self.url.format(**url_format_kw), params=params, http_method=self.http_method
48 | )
49 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_account_info.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import Request
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types import UserProfile
8 |
9 |
10 | class GetUserProfile(QiwiAPIMethod[UserProfile]):
11 | url: ClassVar[str] = 'https://edge.qiwi.com/person-profile/v1/profile/current'
12 | http_method: ClassVar[str] = 'GET'
13 |
14 | include_auth_info: bool = Field(True, alias='authInfoEnabled')
15 | include_contract_info: bool = Field(True, alias='contractInfoEnabled')
16 | include_user_info: bool = Field(True, alias='userInfoEnabled')
17 |
18 | def build_request(self, **url_format_kw: Any) -> 'Request':
19 | return Request(
20 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
21 | http_method=self.http_method,
22 | params={
23 | 'authInfoEnabled': str(self.include_auth_info),
24 | 'contractInfoEnabled': str(self.include_contract_info),
25 | 'userInfoEnabled': str(self.include_user_info),
26 | },
27 | )
28 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_available_balances.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 | from glQiwiApi.qiwi.clients.wallet.types import Balance
5 | from glQiwiApi.qiwi.clients.wallet.types.balance import AvailableBalance
6 |
7 |
8 | class GetAvailableBalances(QiwiAPIMethod[List[AvailableBalance]]):
9 | http_method: ClassVar[str] = 'GET'
10 | url: ClassVar[
11 | str
12 | ] = 'https://edge.qiwi.com/funding-sources/v2/persons/{phone_number}/accounts/offer'
13 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_balances.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List
2 |
3 | from pydantic import parse_obj_as
4 |
5 | from glQiwiApi.core.abc.api_method import ReturningType
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types import Balance
9 |
10 |
11 | class GetBalances(QiwiAPIMethod[List[Balance]]):
12 | http_method: ClassVar[str] = 'GET'
13 | url: ClassVar[str] = 'https://edge.qiwi.com/funding-sources/v2/persons/{phone_number}/accounts'
14 |
15 | @classmethod
16 | def on_json_parse(cls, response: HTTPResponse) -> List[Balance]:
17 | return parse_obj_as(List[Balance], response.json()['accounts'])
18 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_cards.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List, Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 | from glQiwiApi.qiwi.clients.wallet.types import Card
7 |
8 |
9 | class GetBoundedCards(QiwiAPIMethod[List[Card]]):
10 | http_method: ClassVar[str] = 'GET'
11 | url: ClassVar[str] = 'https://edge.qiwi.com/cards/v1/cards'
12 |
13 | card_alias: Optional[str] = Field(None, alias='vas-alias')
14 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_cross_rates.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List
2 |
3 | from pydantic import parse_obj_as
4 |
5 | from glQiwiApi.core.abc.api_method import ReturningType
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types import CrossRate
9 |
10 |
11 | class GetCrossRates(QiwiAPIMethod[List[CrossRate]]):
12 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/crossRates'
13 | http_method: ClassVar[str] = 'GET'
14 |
15 | @classmethod
16 | def on_json_parse(cls, response: HTTPResponse) -> List[CrossRate]:
17 | return parse_obj_as(List[CrossRate], response.json()['result'])
18 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_identification.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 | from glQiwiApi.qiwi.clients.wallet.types import Identification
5 |
6 |
7 | class GetIdentification(QiwiAPIMethod[Identification]):
8 | http_method: ClassVar[str] = 'GET'
9 | url: ClassVar[
10 | str
11 | ] = 'https://edge.qiwi.com/identification/v1/persons/{phone_number}/identification'
12 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_limits.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, List, Sequence
2 |
3 | from glQiwiApi.core.abc.api_method import Request, ReturningType
4 | from glQiwiApi.core.session.holder import HTTPResponse
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 | from glQiwiApi.qiwi.clients.wallet.types import Limit
7 |
8 | ALL_LIMIT_TYPES = [
9 | 'TURNOVER',
10 | 'REFILL',
11 | 'PAYMENTS_P2P',
12 | 'PAYMENTS_PROVIDER_INTERNATIONALS',
13 | 'PAYMENTS_PROVIDER_PAYOUT',
14 | 'WITHDRAW_CASH',
15 | ]
16 |
17 |
18 | class GetLimits(QiwiAPIMethod[Dict[str, Limit]]):
19 | http_method: ClassVar[str] = 'GET'
20 | url: ClassVar[str] = 'https://edge.qiwi.com/qw-limits/v1/persons/{phone_number}/actual-limits'
21 |
22 | limit_types: Sequence[str] = ALL_LIMIT_TYPES
23 |
24 | @classmethod
25 | def parse_http_response(cls, response: HTTPResponse) -> Dict[str, Limit]:
26 | return {
27 | code: [Limit.parse_obj(limit) for limit in limits]
28 | for code, limits in response.json()['limits'].items()
29 | }
30 |
31 | def build_request(self, **url_format_kw: Any) -> Request:
32 | return Request(
33 | endpoint=self.url.format(**url_format_kw),
34 | params={
35 | f'types[{index}]': limit_type for index, limit_type in enumerate(self.limit_types)
36 | },
37 | http_method=self.http_method,
38 | )
39 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_nickname.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 | from glQiwiApi.qiwi.clients.wallet.types.nickname import NickName
5 |
6 |
7 | class GetNickName(QiwiAPIMethod[NickName]):
8 | url: ClassVar[str] = 'https://edge.qiwi.com/qw-nicknames/v1/persons/{phone_number}/nickname'
9 | http_method: ClassVar[str] = 'GET'
10 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/get_receipt.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Union
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.session.holder import HTTPResponse
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types import TransactionType
8 | from glQiwiApi.types.arbitrary import BinaryIOInput, File
9 |
10 |
11 | class GetReceipt(QiwiAPIMethod[File]):
12 | url: ClassVar[
13 | str
14 | ] = 'https://edge.qiwi.com/payment-history/v1/transactions/{transaction_id}/cheque/file'
15 | http_method: ClassVar[str] = 'GET'
16 |
17 | transaction_id: Union[str, int] = Field(..., path_runtime_value=True)
18 | transaction_type: TransactionType = Field(..., alias='type')
19 | file_format: str = Field(alias='format', default='PDF')
20 |
21 | class Config:
22 | use_enum_values = True
23 |
24 | @classmethod
25 | def parse_http_response(cls, response: HTTPResponse) -> File: # type: ignore
26 | return File(BinaryIOInput.from_bytes(response.body))
27 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/list_of_invoices.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, List
2 |
3 | from pydantic import conint, parse_obj_as
4 |
5 | from glQiwiApi.core.session.holder import HTTPResponse
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.p2p.types import Bill
8 |
9 | MAX_INVOICES_LIMIT = 50
10 |
11 |
12 | class GetListOfInvoices(QiwiAPIMethod[List[Bill]]):
13 | url: ClassVar[str] = 'https://edge.qiwi.com/checkout-api/api/bill/search'
14 | http_method: ClassVar[str] = 'GET'
15 |
16 | rows: conint(le=MAX_INVOICES_LIMIT, strict=True, gt=0) = MAX_INVOICES_LIMIT
17 | statuses: str = 'READY_FOR_PAY'
18 |
19 | @classmethod
20 | def on_json_parse(cls, response: HTTPResponse) -> List[Bill]:
21 | return parse_obj_as(List[Bill], response.json()['bills'])
22 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/pay_invoice.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import RuntimeValue
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.p2p.types import InvoiceStatus
8 |
9 |
10 | class PayInvoice(QiwiAPIMethod[InvoiceStatus]):
11 | url: ClassVar[str] = 'https://edge.qiwi.com/checkout-api/invoice/pay/wallet'
12 | http_method: ClassVar[str] = 'POST'
13 |
14 | json_payload_schema: ClassVar[Dict[str, Any]] = {
15 | 'invoice_uid': RuntimeValue(),
16 | 'currency': RuntimeValue(),
17 | }
18 |
19 | invoice_uid: str = Field(..., schema_path='invoice_uid')
20 | currency: str = Field(..., schema_path='currency')
21 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/payment_by_details.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import Any, ClassVar, Dict, Optional
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.abc.api_method import RuntimeValue
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types import PaymentDetails, PaymentInfo, PaymentMethod
9 | from glQiwiApi.types.amount import AmountWithCurrency
10 |
11 |
12 | class MakePaymentByDetails(QiwiAPIMethod[PaymentInfo]):
13 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/api/v2/terms/1717/payments'
14 | http_method: ClassVar[str] = 'POST'
15 |
16 | json_payload_schema: ClassVar[Dict[str, Any]] = {
17 | 'id': RuntimeValue(default_factory=lambda: str(uuid.uuid4())),
18 | 'sum': RuntimeValue(),
19 | 'paymentMethod': RuntimeValue(),
20 | 'fields': RuntimeValue(),
21 | }
22 |
23 | payment_sum: AmountWithCurrency = Field(..., scheme_path='sum')
24 | payment_method: PaymentMethod = Field(..., scheme_path='paymentMethod')
25 | details: PaymentDetails = Field(..., scheme_path='fields')
26 | payment_id: Optional[str] = Field(None, scheme_path='id')
27 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/predict_comission.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict, Optional, Union
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import RuntimeValue
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types import Commission
8 |
9 |
10 | class PredictCommission(QiwiAPIMethod[Commission]):
11 | http_method: ClassVar[str] = 'POST'
12 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/providers/{private_card_id}/onlineCommission'
13 |
14 | json_payload_schema: ClassVar[Dict[str, Any]] = {
15 | 'account': RuntimeValue(),
16 | 'paymentMethod': {'type': 'Account', 'accountId': '643'},
17 | 'purchaseTotals': {'total': {'amount': RuntimeValue(), 'currency': '643'}},
18 | }
19 |
20 | private_card_id: str = Field(..., path_runtime_value=True)
21 |
22 | invoice_amount: Union[str, int, float] = Field(..., scheme_path='purchaseTotals.total.amount')
23 | to_account: str = Field(..., scheme_path='account')
24 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/block_card.py:
--------------------------------------------------------------------------------
1 | import http
2 | from typing import Any, ClassVar, Dict, Sequence
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.abc.api_method import ReturningType
7 | from glQiwiApi.core.session.holder import HTTPResponse
8 | from glQiwiApi.qiwi.base import QiwiAPIMethod
9 |
10 |
11 | class BlockQiwiMasterCard(QiwiAPIMethod[Dict[str, Any]]):
12 | url: ClassVar[str] = '/cards/v2/persons/{phone_number}/cards/{card_id}/block'
13 | http_method: ClassVar[str] = 'GET'
14 |
15 | arbitrary_allowed_response_status_codes: ClassVar[Sequence[int]] = (http.HTTPStatus.ACCEPTED,)
16 |
17 | card_id: str = Field(..., path_runtime_value=True)
18 | phone_number: str = Field(..., path_runtime_value=True)
19 |
20 | @classmethod
21 | def parse_http_response(cls, response: HTTPResponse) -> Dict[str, Any]:
22 | return {}
23 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/buy_qiwi_card.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Any, ClassVar, Dict
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.abc.api_method import RuntimeValue
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types.qiwi_master import OrderDetails
9 |
10 |
11 | class BuyQiwiMasterCard(QiwiAPIMethod[OrderDetails]):
12 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/api/v2/terms/32064/payments'
13 | http_method: ClassVar[str] = 'POST'
14 |
15 | json_payload_schema: ClassVar[Dict[str, Any]] = {
16 | 'id': RuntimeValue(default_factory=lambda: str(int(time.time() * 1000))),
17 | 'sum': {'amount': RuntimeValue(default=99), 'currency': '643'},
18 | 'paymentMethod': {'type': 'Account', 'accountId': '643'},
19 | 'fields': {'account': RuntimeValue(), 'order_id': RuntimeValue()},
20 | }
21 |
22 | phone_number: str = Field(..., schema_path='fields.account')
23 | order_id: str = Field(..., schema_path='fields.order_id')
24 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/buy_qiwi_master.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Any, ClassVar, Dict
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.abc.api_method import RuntimeValue
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types.payment_info import PaymentInfo
9 |
10 |
11 | class BuyQIWIMasterPackage(QiwiAPIMethod[PaymentInfo]):
12 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/api/v2/terms/28004/payments'
13 | http_method: ClassVar[str] = 'POST'
14 |
15 | json_payload_schema: ClassVar[Dict[str, Any]] = {
16 | 'id': RuntimeValue(default_factory=lambda: str(int(time.time() * 1000))),
17 | 'sum': {'amount': RuntimeValue(default=2999), 'currency': '643'},
18 | 'paymentMethod': {'type': 'Account', 'accountId': '643'},
19 | 'comment': 'Оплата',
20 | 'fields': {'account': RuntimeValue(), 'vas_alias': 'qvc-master'},
21 | }
22 |
23 | phone_number: str = Field(..., scheme_path='fields.account')
24 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/confirm_qiwi_master.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 | from glQiwiApi.qiwi.clients.wallet.types.qiwi_master import OrderDetails
7 |
8 |
9 | class ConfirmQiwiMasterPurchaseOrder(QiwiAPIMethod[OrderDetails]):
10 | url: ClassVar[
11 | str
12 | ] = 'https://edge.qiwi.com/cards/v2/persons/{phone_number}/orders/{order_id}/submit'
13 | http_method: ClassVar[str] = 'PUT'
14 |
15 | order_id: str = Field(..., path_runtime_value=True)
16 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/create_card_purchase_order.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import RuntimeValue
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types import OrderDetails
8 |
9 |
10 | class CreateCardPurchaseOrder(QiwiAPIMethod[OrderDetails]):
11 | url: ClassVar[str] = 'https://edge.qiwi.com/cards/v2/persons/{phone_number}/orders'
12 | http_method: ClassVar[str] = 'POST'
13 |
14 | json_payload_schema: ClassVar[Dict[str, Any]] = {'cardAlias': RuntimeValue()}
15 |
16 | card_alias: str = Field(..., schema_path='cardAlias')
17 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/get_card_requisites.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import ClassVar, Optional
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types.qiwi_master import QiwiMasterCardRequisites
8 |
9 |
10 | class GetQiwiMasterCardRequisites(QiwiAPIMethod[QiwiMasterCardRequisites]):
11 | http_method: ClassVar[str] = 'PUT'
12 | url: ClassVar[str] = 'https://edge.qiwi.com/cards/v1/cards/{card_id}/details'
13 |
14 | card_id: str = Field(..., path_runtime_value=True)
15 | operation_id: Optional[str] = Field(
16 | default_factory=lambda: str(uuid.uuid4()), alias='operationId'
17 | )
18 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/get_statement.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import ClassVar
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.types.arbitrary import BinaryIOInput, File
9 |
10 |
11 | class GetQiwiMasterStatement(QiwiAPIMethod[File]):
12 | url: ClassVar[
13 | str
14 | ] = 'https://edge.qiwi.com/payment-history/v1/persons/{phone_number}/cards/{card_id}/statement'
15 | http_method: ClassVar[str] = 'GET'
16 |
17 | card_id: str = Field(..., path_runtime_value=True)
18 | from_date: datetime = Field(..., alias='from')
19 | till_date: datetime = Field(..., alias='till')
20 |
21 | @classmethod
22 | def parse_http_response(cls, response: HTTPResponse) -> File: # type: ignore
23 | return File(BinaryIOInput.from_bytes(response.body))
24 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/rename_card.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import RuntimeValue
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 |
8 |
9 | class RenameQiwiMasterCard(QiwiAPIMethod[Dict[str, Any]]):
10 | url: ClassVar[str] = 'https://edge.qiwi.com/cards/v1/cards/{card_id}>/alias'
11 | http_method: ClassVar[str] = 'PUT'
12 |
13 | json_payload_schema = {'alias': RuntimeValue()}
14 |
15 | card_id: str = Field(..., path_runtime_value=True)
16 | alias: str
17 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/qiwi_master/unblock_card.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 |
7 |
8 | class UnblockQiwiMasterCard(QiwiAPIMethod[Dict[str, Any]]):
9 | url: ClassVar[
10 | str
11 | ] = 'https://edge.qiwi.com/cards/v2/persons/{phone_number}/cards/{card_id}/unblock'
12 | http_method: ClassVar[str] = 'GET'
13 |
14 | card_id: str = Field(..., path_runtime_value=True)
15 | phone_number: str = Field(..., path_runtime_value=True)
16 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/reveal_card_id.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import Request
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 |
9 | try:
10 | from orjson import JSONDecodeError as OrjsonDecodeError
11 | except ImportError:
12 | from json import JSONDecodeError as OrjsonDecodeError # type: ignore
13 |
14 |
15 | class RevealCardID(QiwiAPIMethod[str]):
16 | http_method: ClassVar[str] = 'POST'
17 | url: ClassVar[str] = 'https://qiwi.com/card/detect.action'
18 |
19 | card_number: str = Field(..., alias='cardNumber')
20 |
21 | @classmethod
22 | def on_json_parse(cls, response: HTTPResponse) -> str:
23 | return response.json()['message']
24 |
25 | def build_request(self, **url_format_kw: Any) -> 'Request':
26 | r = super().build_request(**url_format_kw)
27 | r.headers['Content-Type'] = 'application/x-www-form-urlencoded'
28 | return r
29 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/set_default_balance.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.qiwi.base import QiwiAPIMethod
6 |
7 |
8 | class SetDefaultBalance(QiwiAPIMethod[Dict[Any, Any]]):
9 | http_method: ClassVar[str] = 'PATCH'
10 | url: ClassVar[
11 | str
12 | ] = 'https://edge.qiwi.com/funding-sources/v2/persons/{phone_number}/accounts/{account_alias}'
13 |
14 | account_alias: str = Field(..., path_runtime_value=True)
15 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/transaction_info.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import Request
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types import Transaction, TransactionType
8 |
9 |
10 | class GetTransactionInfo(QiwiAPIMethod[Transaction]):
11 | http_method: ClassVar[str] = 'GET'
12 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-history/v1/transactions/{transaction_id}'
13 |
14 | transaction_id: int
15 | transaction_type: TransactionType = Field(..., alias='type')
16 |
17 | def build_request(self, **url_format_kw: Any) -> 'Request':
18 | return super().build_request(**url_format_kw, transaction_id=self.transaction_id)
19 |
20 | class Config:
21 | use_enum_values = True
22 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/transfer_money.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Any, ClassVar, Dict, Optional
3 |
4 | from pydantic import Field, validator
5 |
6 | from glQiwiApi.core.abc.api_method import RuntimeValue
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types import PaymentInfo
9 |
10 |
11 | class TransferMoney(QiwiAPIMethod[PaymentInfo]):
12 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/api/v2/terms/99/payments'
13 | http_method: ClassVar[str] = 'POST'
14 |
15 | json_payload_schema: ClassVar[Dict[str, Any]] = {
16 | 'id': RuntimeValue(default_factory=lambda: str(int(time.time() * 1000))),
17 | 'sum': {'amount': RuntimeValue(), 'currency': '643'},
18 | 'paymentMethod': {'type': 'Account', 'accountId': '643'},
19 | 'comment': RuntimeValue(mandatory=False),
20 | 'fields': {'account': RuntimeValue()},
21 | }
22 |
23 | @validator('to_wallet')
24 | def add_plus_sign_to_phone_number(cls, v: str) -> str:
25 | if v.startswith('+'):
26 | return v
27 | return f'+{v}'
28 |
29 | amount: float = Field(..., scheme_path='sum.amount')
30 | to_wallet: str = Field(..., scheme_path='fields.account')
31 | comment: Optional[str] = Field(None, scheme_path='comment')
32 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/transfer_money_to_card.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Any, ClassVar, Dict, Union
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.core.abc.api_method import Request, RuntimeValue
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 | from glQiwiApi.qiwi.clients.wallet.types import PaymentInfo
9 |
10 |
11 | class TransferMoneyToCard(QiwiAPIMethod[PaymentInfo]):
12 | http_method: ClassVar[str] = 'POST'
13 | url: ClassVar[str] = 'https://edge.qiwi.com/sinap/api/v2/terms/{private_card_id}/payments'
14 |
15 | json_payload_schema: ClassVar[Dict[str, Any]] = {
16 | 'id': RuntimeValue(default_factory=lambda: str(int(time.time() * 1000))),
17 | 'sum': {'amount': RuntimeValue(), 'currency': '643'},
18 | 'paymentMethod': {'type': 'Account', 'accountId': '643'},
19 | 'fields': {'account': RuntimeValue()},
20 | }
21 |
22 | private_card_id: str = Field(..., path_runtime_value=True)
23 | amount: float = Field(..., scheme_path='sum.amount')
24 | card_number: str = Field(..., scheme_path='fields.account')
25 | kwargs: Dict[str, Any] = {} # parameters for cards with ID 1960, 21012
26 |
27 | def build_request(self, **url_format_kw: Any) -> 'Request':
28 | request_schema = self._get_filled_json_payload_schema()
29 | request_schema['fields'].update(**self.kwargs)
30 | return Request(
31 | json_payload=request_schema,
32 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
33 | http_method=self.http_method,
34 | )
35 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/qiwi/clients/wallet/methods/webhook/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/change_webhook_secret.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import ReturningType
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 |
9 |
10 | class GenerateWebhookSecret(QiwiAPIMethod[str]):
11 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-notifier/v1/hooks/{hook_id}/newkey'
12 | http_method: ClassVar[str] = 'POST'
13 |
14 | hook_id: str = Field(..., path_runtime_value=True)
15 |
16 | @classmethod
17 | def on_json_parse(cls, response: HTTPResponse) -> ReturningType:
18 | return response.json()['key']
19 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/delete_current_webhook.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from glQiwiApi.core.abc.api_method import Request
4 | from glQiwiApi.qiwi.base import QiwiAPIMethod
5 |
6 |
7 | class DeleteWebhook(QiwiAPIMethod[Dict[Any, Any]]):
8 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-notifier/v1/hooks/{hook_id}'
9 | http_method: ClassVar[str] = 'DELETE'
10 |
11 | hook_id: str
12 |
13 | def build_request(self, **url_format_kw: Any) -> Request:
14 | return super().build_request(**url_format_kw, hook_id=self.hook_id)
15 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/get_current_webhook.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 | from glQiwiApi.qiwi.clients.wallet.types import WebhookInfo
5 |
6 |
7 | class GetCurrentWebhook(QiwiAPIMethod[WebhookInfo]):
8 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-notifier/v1/hooks/active'
9 | http_method: ClassVar[str] = 'GET'
10 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/get_webhook_secret.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import ReturningType
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 | from glQiwiApi.qiwi.base import QiwiAPIMethod
8 |
9 |
10 | class GetWebhookSecret(QiwiAPIMethod[str]):
11 | http_method: ClassVar[str] = 'GET'
12 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-notifier/v1/hooks/{hook_id}/key'
13 |
14 | hook_id: str = Field(..., path_runtime_value=True)
15 |
16 | @classmethod
17 | def on_json_parse(cls, response: HTTPResponse) -> str:
18 | return response.json()['key']
19 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/register_webhook.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import Request
6 | from glQiwiApi.qiwi.base import QiwiAPIMethod
7 | from glQiwiApi.qiwi.clients.wallet.types import WebhookInfo
8 |
9 |
10 | class RegisterWebhook(QiwiAPIMethod[WebhookInfo]):
11 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-notifier/v1/hooks'
12 | http_method: ClassVar[str] = 'PUT'
13 |
14 | webhook_url: str = Field(..., alias='param')
15 | txn_type: int = Field(..., alias='txnType')
16 |
17 | hook_type: int = Field(default=1, alias='hookType')
18 |
19 | def build_request(self, **url_format_kw: Any) -> 'Request':
20 | return Request(
21 | endpoint=self.url.format(**url_format_kw),
22 | params=self.dict(by_alias=True),
23 | http_method=self.http_method,
24 | )
25 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/methods/webhook/send_test_notification.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from glQiwiApi.qiwi.base import QiwiAPIMethod
4 |
5 |
6 | class SendTestWebhookNotification(QiwiAPIMethod[Dict[Any, Any]]):
7 | http_method: ClassVar[str] = 'GET'
8 | url: ClassVar[str] = 'https://edge.qiwi.com/payment-notifier/v1/hooks/test'
9 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/__init__.py:
--------------------------------------------------------------------------------
1 | from .account_info import UserProfile
2 | from .balance import Balance
3 | from .commission import Commission
4 | from .identification import Identification
5 | from .limit import Limit
6 | from .other import CrossRate, PaymentDetails, PaymentMethod
7 | from .partner import Partner
8 | from .payment_info import PaymentInfo, QiwiPayment
9 | from .qiwi_master import Card, OrderDetails
10 | from .restriction import Restriction
11 | from .stats import Statistic
12 | from .transaction import History, Source, Transaction, TransactionStatus, TransactionType
13 | from .webhooks import TransactionWebhook, WebhookInfo
14 |
15 | __all__ = (
16 | 'UserProfile',
17 | 'Transaction',
18 | 'Statistic',
19 | 'Limit',
20 | 'Balance',
21 | 'Identification',
22 | 'PaymentInfo',
23 | 'OrderDetails',
24 | 'Partner',
25 | 'TransactionWebhook',
26 | 'WebhookInfo',
27 | 'CrossRate',
28 | 'PaymentMethod',
29 | 'Card',
30 | 'Restriction',
31 | 'Commission',
32 | 'TransactionType',
33 | 'QiwiPayment',
34 | 'TransactionStatus',
35 | 'Source',
36 | 'PaymentDetails',
37 | 'History',
38 | )
39 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/balance.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional, Union
2 |
3 | from pydantic import Field, validator
4 |
5 | from glQiwiApi.types.amount import AmountWithCurrency, CurrencyModel
6 | from glQiwiApi.types.base import HashableBase
7 | from glQiwiApi.utils.currency_util import Currency
8 |
9 |
10 | class AvailableBalance(HashableBase):
11 | alias: str
12 | currency: Union[str, CurrencyModel]
13 |
14 |
15 | class Balance(HashableBase):
16 | """object: Balance"""
17 |
18 | alias: str
19 | title: str
20 | fs_alias: str = Field(alias='fsAlias')
21 | bank_alias: str = Field(alias='bankAlias')
22 | has_balance: bool = Field(alias='hasBalance')
23 | balance: Optional[AmountWithCurrency] = None
24 | currency: CurrencyModel
25 | account_type: Optional[Dict[str, Any]] = Field(None, alias='type')
26 | is_default_account: bool = Field(alias='defaultAccount')
27 |
28 | @validator('currency', pre=True)
29 | def humanize_pay_currency(cls, v): # type: ignore
30 | if not isinstance(v, int):
31 | return v
32 | return Currency.get(str(v))
33 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/commission.py:
--------------------------------------------------------------------------------
1 | from pydantic import Field
2 |
3 | from glQiwiApi.types.amount import AmountWithCurrency
4 | from glQiwiApi.types.base import Base
5 |
6 |
7 | class Commission(Base):
8 | provider_id: int = Field(alias='providerId')
9 | withdraw_sum: AmountWithCurrency = Field(alias='withdrawSum')
10 | enrollment_sum: AmountWithCurrency = Field(alias='enrollmentSum')
11 | qiwi_commission: AmountWithCurrency = Field(alias='qwCommission')
12 | withdraw_to_enrollment_rate: int = Field(alias='withdrawToEnrollmentRate')
13 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/identification.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | from typing import Optional
3 |
4 | from pydantic import Field
5 |
6 | from glQiwiApi.types.base import Base
7 |
8 |
9 | class Identification(Base):
10 | """object: Identification"""
11 |
12 | identification_id: int = Field(..., alias='id')
13 | first_name: str = Field(..., alias='firstName')
14 | middle_name: str = Field(..., alias='middleName')
15 | last_name: str = Field(..., alias='lastName')
16 | birth_date: date = Field(..., alias='birthDate')
17 | passport: str
18 | inn: Optional[str]
19 | snils: Optional[str]
20 | oms: Optional[str]
21 | type: str
22 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/limit.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Union
3 |
4 | from pydantic import Field, validator
5 |
6 | from glQiwiApi.types.amount import CurrencyModel
7 | from glQiwiApi.types.base import Base
8 | from glQiwiApi.utils.currency_util import Currency
9 |
10 |
11 | class Interval(Base):
12 | """object: Interval"""
13 |
14 | date_from: datetime = Field(alias='dateFrom')
15 | date_till: datetime = Field(alias='dateTill')
16 |
17 |
18 | class Limit(Base):
19 | """object: Limit"""
20 |
21 | currency: CurrencyModel
22 | rest: Union[float, int]
23 | max_limit: Union[float, int] = Field(alias='max')
24 | spent: Union[float, int]
25 | interval: Interval
26 | limit_type: str = Field(alias='type')
27 |
28 | @validator('currency', pre=True)
29 | def currency_validate(cls, v): # type: ignore
30 | if not isinstance(v, str):
31 | raise ValueError()
32 | return Currency.get(v)
33 |
34 |
35 | __all__ = ['Limit']
36 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/mobile_operator.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional
2 |
3 | from pydantic import Field, Json
4 |
5 | from glQiwiApi.types.base import Base
6 |
7 |
8 | class Code(Base):
9 | value: str
10 | name: str = Field(..., alias='_name')
11 |
12 |
13 | class MobileOperator(Base):
14 | code: Code
15 | data: Optional[Json]
16 | message: str
17 | messages: Optional[Dict[str, Any]]
18 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/nickname.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.types.base import Base
6 |
7 |
8 | class NickName(Base):
9 | can_change: bool = Field(..., alias='canChange')
10 | can_use: bool = Field(..., alias='canUse')
11 | description: str
12 | nickname: Optional[str] = None
13 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/other.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 | from pydantic import Field, validator
4 |
5 | from glQiwiApi.types.amount import CurrencyModel
6 | from glQiwiApi.types.base import Base, HashableBase
7 | from glQiwiApi.utils.currency_util import Currency
8 |
9 |
10 | class CrossRate(HashableBase):
11 | """Курс валюты"""
12 |
13 | rate_from: Union[str, CurrencyModel] = Field(..., alias='from')
14 | rate_to: Union[str, CurrencyModel] = Field(..., alias='to')
15 | rate: float
16 |
17 | @validator('rate_from', 'rate_to', pre=True)
18 | def humanize_rates(cls, v): # type: ignore
19 | if not isinstance(v, str):
20 | return v
21 | cur = Currency.get(v)
22 | if not cur:
23 | return v
24 | return cur
25 |
26 |
27 | class PaymentMethod(Base):
28 | payment_type: str
29 | account_id: str
30 |
31 |
32 | class PaymentDetails(Base):
33 | """Набор реквизитов платежа"""
34 |
35 | name: str
36 | """Наименование банка получателя"""
37 |
38 | extra_to_bik: str
39 | """БИК банка получателя"""
40 |
41 | to_bik: str
42 | """ БИК банка получателя"""
43 |
44 | city: str
45 | """Город местонахождения получателя"""
46 |
47 | info: str = 'Коммерческие организации'
48 | """Константное значение"""
49 |
50 | is_commercial: str = '1'
51 | """Служебная информация"""
52 |
53 | to_name: str
54 | """Наименование организации"""
55 |
56 | to_inn: str
57 | """ИНН организации"""
58 |
59 | to_kpp: str
60 | """ КПП организации"""
61 |
62 | nds: str
63 | """
64 | Признак уплаты НДС.
65 | Если вы оплачиваете квитанцию и в ней не указан НДС,
66 | то строка НДС не облагается. В ином случае, строка В т.ч. НДС
67 | """
68 |
69 | goal: str
70 | """Назначение платежа"""
71 |
72 | urgent: str = '0'
73 | """
74 | Признак срочного платежа (0 - нет, 1 - да).
75 | Срочный платеж выполняется от 10 минут.
76 | Возможен по будням с 9:00 до 20:30 по московскому времени.
77 | Стоимость услуги — 25 рублей.
78 | """
79 |
80 | account: str
81 | """Номер счета получателя"""
82 |
83 | from_name: str
84 | """Имя плательщика"""
85 |
86 | from_name_p: str
87 | """Отчество плательщика"""
88 |
89 | from_name_f: str
90 | """ Фамилия плательщика"""
91 |
92 | requestProtocol: str = 'qw1'
93 | """Служебная информация, константа"""
94 |
95 | toServiceId: str = '1717'
96 | """Служебная информация, константа"""
97 |
98 |
99 | __all__ = ('CrossRate', 'PaymentDetails', 'PaymentMethod')
100 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/partner.py:
--------------------------------------------------------------------------------
1 | """Main model: Partner"""
2 | from typing import List, Optional
3 |
4 | from glQiwiApi.types.base import Base
5 |
6 |
7 | class Partner(Base):
8 | """Base partner class"""
9 |
10 | title: str
11 | id: int
12 |
13 | maps: Optional[List[str]] = None
14 |
15 |
16 | __all__ = ('Partner',)
17 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/payment_info.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.types.amount import AmountWithCurrency
6 | from glQiwiApi.types.base import Base
7 |
8 |
9 | class Fields(Base):
10 | """object: Fields"""
11 |
12 | account: str
13 |
14 |
15 | class State(Base):
16 | """object: State"""
17 |
18 | code: str
19 |
20 |
21 | class TransactionInfo(Base):
22 | """object: TransactionInfo"""
23 |
24 | id: int
25 | state: State
26 |
27 |
28 | class PaymentInfo(Base):
29 | """object: PaymentInfo"""
30 |
31 | id: int
32 | amount: AmountWithCurrency = Field(..., alias='sum')
33 | terms: str
34 | fields: Fields
35 | source: str
36 | transaction: Optional[TransactionInfo] = None
37 | comment: Optional[str] = None
38 |
39 |
40 | class PaymentMethod(Base):
41 | type: str = 'Account'
42 | account_id: int = Field(643, alias='accountId')
43 |
44 |
45 | class QiwiPayment(Base):
46 | id: int
47 | sum: AmountWithCurrency
48 | method: PaymentMethod = Field(..., alias='paymentMethod')
49 | fields: Dict[Any, Any]
50 | comment: Optional[str] = None
51 |
52 |
53 | __all__ = ['PaymentInfo', 'QiwiPayment']
54 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/qiwi_master.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import datetime
4 | from typing import List, Optional
5 |
6 | from pydantic import Field
7 |
8 | from glQiwiApi.types.amount import AmountWithCurrency
9 | from glQiwiApi.types.base import Base, HashableBase
10 |
11 |
12 | class QiwiMasterCardRequisites(Base):
13 | status: str
14 | cvv: str
15 | pan: str
16 |
17 |
18 | class OrderDetails(Base):
19 | order_id: str = Field(..., alias='id')
20 | card_alias: str = Field(..., alias='cardAlias')
21 | status: str
22 | price: Optional[AmountWithCurrency] = None
23 | card_id: Optional[str] = Field(alias='cardId', default=None)
24 |
25 |
26 | class CardCredentials(Base):
27 | qvx_id: int = Field(..., alias='id')
28 | masked_pan: str = Field(..., alias='maskedPan')
29 | status: str
30 | card_expire: datetime.datetime = Field(..., alias='cardExpire')
31 | card_type: str = Field(..., alias='cardType')
32 | card_alias: Optional[str] = Field(..., alias='cardAlias')
33 | activated: datetime.datetime
34 | sms_recender: Optional[datetime.datetime] = Field(..., alias='smsResended')
35 | post_number: Optional[str] = Field(default=None, alias='postNumber')
36 | blocked_date: Optional[datetime.datetime] = Field(default=None, alias='blockedDate')
37 | card_id: int = Field(..., alias='cardId')
38 | txn_id: int = Field(..., alias='txnId')
39 | card_expire_month: str = Field(..., alias='cardExpireMonth')
40 | card_expire_year: str = Field(..., alias='cardExpireYear')
41 |
42 |
43 | class Requisite(Base):
44 | name: str
45 | value: str
46 |
47 |
48 | class Details(Base):
49 | info: str
50 | description: str
51 | tariff_link: str = Field(..., alias='tariffLink')
52 | offer_link: str = Field(..., alias='offerLink')
53 | features: List[str]
54 | requisites: List[Requisite]
55 |
56 |
57 | class CardInfo(Base):
58 | id_: int = Field(..., alias='id')
59 | name: str
60 | alias: str
61 | price: AmountWithCurrency
62 | period: str
63 | type_: str = Field(..., alias='type')
64 | details: Details
65 |
66 |
67 | class Card(HashableBase):
68 | details: CardCredentials = Field(..., alias='qvx')
69 | balance: Optional[AmountWithCurrency] = None
70 | info: CardInfo
71 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/restriction.py:
--------------------------------------------------------------------------------
1 | from pydantic import Field
2 |
3 | from glQiwiApi.types.base import HashableBase
4 |
5 |
6 | class Restriction(HashableBase):
7 | code: str = Field(..., alias='restrictionCode')
8 | description: str = Field(..., alias='restrictionDescription')
9 |
10 |
11 | __all__ = ('Restriction',)
12 |
--------------------------------------------------------------------------------
/glQiwiApi/qiwi/clients/wallet/types/stats.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.types.amount import AmountWithCurrency
6 | from glQiwiApi.types.base import Base
7 |
8 |
9 | class Statistic(Base):
10 | """object: Statistic"""
11 |
12 | incoming: List[AmountWithCurrency] = Field(alias='incomingTotal')
13 | out: List[AmountWithCurrency] = Field(alias='outgoingTotal')
14 |
15 |
16 | __all__ = ['Statistic']
17 |
--------------------------------------------------------------------------------
/glQiwiApi/types/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/types/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/types/amount.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union
2 |
3 | from pydantic import BaseConfig, BaseModel, Field, validator
4 |
5 | from glQiwiApi.types.base import Base, HashableBase
6 |
7 |
8 | class CurrencyModel(HashableBase):
9 | code: str
10 | decimal_digits: int
11 | name: str
12 | name_plural: str
13 | rounding: Union[int, float]
14 | symbol: str
15 | symbol_native: str
16 | iso_format: Optional[str] = Field(..., alias='isoformat')
17 |
18 | def __str__(self) -> str:
19 | return self.code
20 |
21 | class Config(BaseConfig):
22 | frozen = True
23 | allow_mutation = False
24 |
25 |
26 | class AmountWithCurrency(Base):
27 | amount: float
28 | currency: Union[CurrencyModel, str] # string if currency util couldn't parse it
29 |
30 | @validator('currency', pre=True)
31 | def humanize_pay_currency(cls, v): # type: ignore
32 | from glQiwiApi.utils.currency_util import Currency
33 |
34 | if not isinstance(v, int):
35 | try:
36 | v = int(v)
37 | except ValueError:
38 | return v
39 | return Currency.get(str(v))
40 |
41 |
42 | class HashableSum(HashableBase, AmountWithCurrency):
43 | ...
44 |
45 |
46 | class PlainAmount(BaseModel):
47 | value: float
48 | currency: str
49 |
50 |
51 | class HashablePlainAmount(HashableBase, PlainAmount):
52 | ...
53 |
54 |
55 | class Type(BaseModel):
56 | id: str
57 | title: str
58 |
--------------------------------------------------------------------------------
/glQiwiApi/types/arbitrary/__init__.py:
--------------------------------------------------------------------------------
1 | from .file import File
2 | from .inputs import AbstractInput, BinaryIOInput, PathlibPathInput, PlainPathInput
3 |
--------------------------------------------------------------------------------
/glQiwiApi/types/arbitrary/file.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | import inspect
5 | import pathlib
6 | from typing import Any, BinaryIO, Union
7 |
8 | from glQiwiApi.types.arbitrary.inputs import AbstractInput
9 | from glQiwiApi.utils.compat import aiofiles
10 |
11 | CHUNK_SIZE = 65536
12 |
13 | StrOrBytesPath = Union[str, bytes, pathlib.Path] # stable
14 |
15 | _OpenFile = Union[StrOrBytesPath, int]
16 |
17 |
18 | class File:
19 | def __init__(self, input: AbstractInput[Any]) -> None:
20 | self._input = input
21 |
22 | def get_filename(self) -> str:
23 | return self._input.get_filename()
24 |
25 | def get_underlying_file_descriptor(self) -> BinaryIO:
26 | return self._input.get_file()
27 |
28 | def get_path(self) -> str:
29 | return self._input.get_path()
30 |
31 | def save(self, path: StrOrBytesPath, chunk_size: int = CHUNK_SIZE) -> None:
32 | file_descriptor = self.get_underlying_file_descriptor()
33 | with open(path, 'wb') as fp:
34 | while True:
35 | data = file_descriptor.read(chunk_size)
36 | if not data:
37 | break
38 | fp.write(data)
39 | fp.flush()
40 |
41 | if file_descriptor.seekable():
42 | file_descriptor.seek(0)
43 |
44 | async def save_asynchronously(
45 | self, path: StrOrBytesPath, chunk_size: int = CHUNK_SIZE
46 | ) -> None:
47 | file_descriptor = self.get_underlying_file_descriptor()
48 | async with aiofiles.open(path, 'wb') as fp:
49 | while True:
50 | data = file_descriptor.read(chunk_size)
51 | if not data:
52 | break
53 | await fp.write(data)
54 | await fp.flush()
55 |
56 | if file_descriptor.seekable():
57 | file_descriptor.seek(0)
58 |
59 | def __str__(self) -> str:
60 | try:
61 | return self.get_filename()
62 | except TypeError:
63 | return ''
64 |
65 | def __del__(self) -> None:
66 | if not hasattr(self, '_input'):
67 | return
68 |
69 | if inspect.iscoroutinefunction(self._input.close()): # type: ignore # noqa
70 | return asyncio.ensure_future(self._input.close()) # type: ignore # noqa
71 | self._input.close()
72 |
73 | __repr__ = __str__
74 |
--------------------------------------------------------------------------------
/glQiwiApi/types/arbitrary/inputs.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import abc
4 | import io
5 | import os
6 | import pathlib
7 | from types import TracebackType
8 | from typing import Any, BinaryIO, Generic, Optional, Type, TypeVar
9 |
10 | InputType = TypeVar('InputType')
11 |
12 | __all__ = ('AbstractInput', 'PlainPathInput', 'PathlibPathInput', 'BinaryIOInput')
13 |
14 |
15 | class AbstractInput(abc.ABC, Generic[InputType]):
16 | def __init__(self, input_: InputType) -> None:
17 | self._input = input_
18 | self._file_descriptor: Optional[BinaryIO] = None
19 |
20 | @abc.abstractmethod
21 | def get_file(self) -> BinaryIO:
22 | ...
23 |
24 | def get_path(self) -> str:
25 | raise TypeError(
26 | f"{self.__class__.__qualname__} doesn't provide a mechanism to get path to file"
27 | )
28 |
29 | def get_filename(self) -> str:
30 | raise TypeError(
31 | f"{self.__class__.__qualname__} doesn't provide a mechanism to get filename"
32 | )
33 |
34 | def close(self) -> None:
35 | if self._file_descriptor is None:
36 | return None
37 | self._file_descriptor.close()
38 |
39 | def __enter__(self) -> AbstractInput[Any]:
40 | self._file_descriptor = self.get_file()
41 | return self
42 |
43 | def __exit__(
44 | self,
45 | exc_type: Optional[Type[BaseException]],
46 | exc_value: Optional[BaseException],
47 | traceback: Optional[TracebackType],
48 | ) -> None:
49 | self.close()
50 |
51 |
52 | class PlainPathInput(AbstractInput[str]):
53 | def get_file(self) -> BinaryIO:
54 | if pathlib.Path(self._input).is_file() is False:
55 | raise TypeError(f'Input {self._input} is not a file!')
56 | descriptor = open(self._input, 'rb')
57 | self._file_descriptor = descriptor
58 | return descriptor
59 |
60 | def get_path(self) -> str:
61 | return self._input
62 |
63 | def get_filename(self) -> str:
64 | return os.path.split(self._input)[-1]
65 |
66 |
67 | class PathlibPathInput(AbstractInput[pathlib.Path]):
68 | def get_file(self) -> BinaryIO:
69 | descriptor = open(self._input, 'rb')
70 | self._file_descriptor = descriptor
71 | return descriptor
72 |
73 | def get_path(self) -> str:
74 | return str(self._input.resolve())
75 |
76 | def get_filename(self) -> str:
77 | return self._input.name
78 |
79 |
80 | class BinaryIOInput(AbstractInput[BinaryIO]):
81 | def get_file(self) -> BinaryIO:
82 | return self._input
83 |
84 | @classmethod
85 | def from_bytes(cls: Type[BinaryIOInput], b: bytes) -> BinaryIOInput:
86 | return cls(input_=io.BytesIO(b))
87 |
--------------------------------------------------------------------------------
/glQiwiApi/types/base.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseConfig, BaseModel
2 |
3 | from glQiwiApi.utils.compat import json
4 |
5 |
6 | class Base(BaseModel):
7 | class Config(BaseConfig):
8 | json_dumps = json.dumps # type: ignore
9 | json_loads = json.loads
10 | orm_mode = True
11 |
12 |
13 | class HashableBase(Base):
14 | class Config(BaseConfig):
15 | allow_mutation = False
16 | frozen = True
17 |
--------------------------------------------------------------------------------
/glQiwiApi/types/exceptions.py:
--------------------------------------------------------------------------------
1 | class WebhookSignatureUnverifiedError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/utils/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/utils/compat.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from typing import Any, AnyStr, AsyncContextManager
3 |
4 | if sys.version_info >= (3, 9):
5 |
6 | def remove_suffix(input_string: AnyStr, suffix: AnyStr) -> AnyStr:
7 | return input_string.removesuffix(suffix)
8 |
9 | else:
10 |
11 | def remove_suffix(input_string: AnyStr, suffix: AnyStr) -> AnyStr:
12 | """Backport for python 3.9 str.removesuffix(...)"""
13 | if suffix and input_string.endswith(suffix):
14 | return input_string[: -len(suffix)]
15 | return input_string
16 |
17 |
18 | class ModuleNotInstalledException(Exception):
19 | pass
20 |
21 |
22 | class EmptyCls(object):
23 | def __init__(self) -> None:
24 | raise ModuleNotInstalledException()
25 |
26 |
27 | try:
28 | import orjson as json
29 | except ImportError:
30 | import json
31 |
32 | try:
33 | import aiofiles
34 | except ImportError:
35 |
36 | class aiofiles_compat:
37 | def open(self, *args: Any, **kwargs: Any) -> AsyncContextManager[Any]:
38 | raise ModuleNotInstalledException(
39 | "Module aiofiles not installed and you can't use it's "
40 | 'functionality till you install this module.'
41 | )
42 |
43 | aiofiles = aiofiles_compat() # type: ignore
44 |
45 | try:
46 | import orjson as json # noqa
47 | except (ImportError, ModuleNotFoundError):
48 | import json # type: ignore
49 |
50 | try:
51 | from aiogram import Dispatcher
52 | from aiogram.types import InputFile
53 | except (ModuleNotFoundError, ImportError):
54 | Dispatcher = EmptyCls
55 | InputFile = EmptyCls
56 |
57 | if sys.version_info >= (3, 8):
58 | from typing import Final, Literal, Protocol # noqa
59 | else:
60 | from typing_extensions import Final, Literal, Protocol # noqa
61 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/currency_util.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union
2 |
3 | from glQiwiApi.types import _currencies
4 | from glQiwiApi.types.amount import CurrencyModel
5 |
6 |
7 | class Currency:
8 | """
9 | Class with many currencies
10 | import glQiwiApi.types.basics >>> usd = Currency.get('840')
11 | >>> usd
12 | ... CurrencyModel(code='USD', decimal_digits=2, name='US Dollar', name_plural='US dollars',
13 | ... rounding=0, symbol='$', symbol_native='$')
14 |
15 | >>> usd.symbol
16 | ... '$'
17 | """
18 |
19 | @classmethod
20 | def get(cls, currency_code: Union[str, int]) -> Optional[CurrencyModel]:
21 | """
22 | Implements class-based getitem behaviour
23 |
24 | >>> Currency.get('840').symbol
25 | ... '$'
26 | >>> Currency.get('USD').symbol
27 | ... '$'
28 | :param currency_code: ISO 4217 string or CODE
29 | :return: Currency object
30 | """
31 | if isinstance(currency_code, int) or currency_code.isdigit():
32 | return _currencies.described.get(_currencies.codes_number[str(currency_code)])
33 | else:
34 | return _currencies.described.get(currency_code.upper())
35 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/date_conversion.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Optional
3 |
4 | DEFAULT_QIWI_TIMEZONE = 'Europe/Moscow'
5 |
6 | try:
7 | import zoneinfo
8 | except ImportError:
9 | import backports.zoneinfo as zoneinfo
10 |
11 |
12 | def datetime_to_utc_in_iso_format(obj: datetime) -> str:
13 | return obj.astimezone(tz=timezone.utc).isoformat(timespec='milliseconds')
14 |
15 |
16 | def datetime_to_iso8601_with_moscow_timezone(obj: Optional[datetime]) -> str:
17 | """
18 | Converts a date to a standard format for API's
19 |
20 | :param obj: datetime object to parse to string
21 | :return: string - parsed date
22 | """
23 | if not isinstance(obj, datetime):
24 | return '' # pragma: no cover
25 | return localize_datetime_according_to_moscow_timezone(obj).isoformat(timespec='seconds')
26 |
27 |
28 | def localize_datetime_according_to_moscow_timezone(dt: datetime):
29 | return dt.astimezone(zoneinfo.ZoneInfo(DEFAULT_QIWI_TIMEZONE))
30 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/mypy_hacks.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Callable, TypeVar
3 |
4 | T = TypeVar('T')
5 |
6 |
7 | def lru_cache(maxsize: int = 128, typed: bool = False) -> Callable[[T], T]:
8 | """
9 | fix: lru_cache annotation doesn't work with a property
10 | this hack is only needed for the property, so type annotations are as they are
11 | """
12 |
13 | def wrapper(func: T) -> T:
14 | return functools.lru_cache(maxsize, typed)(func) # type: ignore
15 |
16 | return wrapper
17 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/payload.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import Any, Dict, TypeVar, cast
3 |
4 | from pydantic import BaseModel
5 |
6 | Model = TypeVar('Model', bound=BaseModel)
7 | DEFAULT_EXCLUDE = ('cls', 'self', '__class__')
8 |
9 |
10 | def filter_dictionary_none_values(dictionary: Dict[Any, Any]) -> Dict[Any, Any]:
11 | """
12 | Pop NoneType values and convert everything to str, designed?for=params
13 | :param dictionary: source dict
14 | :return: filtered dict
15 | """
16 | return {k: str(v) for k, v in dictionary.items() if v is not None}
17 |
18 |
19 | def make_payload(**kwargs: Any) -> Dict[Any, Any]:
20 | exclude_list = kwargs.pop('exclude', ())
21 | return {
22 | key: value
23 | for key, value in kwargs.items()
24 | if key not in DEFAULT_EXCLUDE + exclude_list and value is not None
25 | }
26 |
27 |
28 | def parse_auth_link(response_data: str) -> str:
29 | """
30 | Parse link for getting code, which needs to be entered in the method
31 | get_access_token
32 | :param response_data:
33 | """
34 | regexp = re.compile(
35 | r'https://yoomoney.ru/oauth2/authorize[?]requestid[=]\w+'
36 | ) # pragma: no cover
37 | return cast(str, re.findall(regexp, str(response_data))[0]) # pragma: no cover
38 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/synchronous/__init__.py:
--------------------------------------------------------------------------------
1 | from .adapter import async_as_sync, execute_async_as_sync
2 |
3 | __all__ = (
4 | 'async_as_sync',
5 | 'execute_async_as_sync',
6 | )
7 |
--------------------------------------------------------------------------------
/glQiwiApi/utils/synchronous/adapter.pyi:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | from concurrent import futures as futures
5 | from concurrent.futures import Future
6 | from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar, Union
7 |
8 | N = TypeVar("N")
9 |
10 | def run_forever_safe(
11 | loop: asyncio.AbstractEventLoop,
12 | callback: Optional[Callable[..., Awaitable[N]]] = None,
13 | ) -> None: ...
14 | def safe_cancel(
15 | loop: asyncio.AbstractEventLoop,
16 | callback: Optional[Callable[..., Awaitable[N]]],
17 | ) -> None: ...
18 |
19 | AnyExecutor = Union[futures.ThreadPoolExecutor, futures.ProcessPoolExecutor, Optional[None]]
20 |
21 | def _cancel_future(
22 | loop: asyncio.AbstractEventLoop,
23 | future: asyncio.Future[N],
24 | executor: AnyExecutor,
25 | ) -> None: ...
26 | def _stop_loop(loop: asyncio.AbstractEventLoop) -> None: ...
27 | def take_event_loop(set_debug: bool = False) -> asyncio.AbstractEventLoop: ...
28 | def await_sync(future: Future[N]) -> N: ...
29 | def execute_async_as_sync(
30 | func: Callable[..., Coroutine[Any, Any, N]], *args: object, **kwargs: object
31 | ) -> N: ...
32 |
33 | class async_as_sync: # NOQA
34 | _async_shutdown_callback: Optional[Callable[..., Awaitable[N]]]
35 | _sync_shutdown_callback: Optional[Callable[[Any], Any]]
36 | def __init__(
37 | self,
38 | async_shutdown_callback: Optional[Callable[..., Awaitable[N]]] = None,
39 | sync_shutdown_callback: Optional[Callable[[Any], Any]] = None,
40 | ) -> None: ...
41 | def __call__(self, func: Callable[..., Awaitable[N]]) -> Callable[..., N]: ...
42 | def execute_sync_callback(self, result: Any) -> Any: ...
43 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import YooMoneyAPI
2 |
3 | __all__ = ('YooMoneyAPI',)
4 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/glQiwiApi/yoo_money/methods/__init__.py
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/acccept_incoming_transfer.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod
4 | from glQiwiApi.yoo_money.types import IncomingTransaction
5 |
6 |
7 | class AcceptIncomingTransfer(APIMethod[IncomingTransaction]):
8 | http_method: ClassVar[str] = 'POST'
9 | url: ClassVar[str] = 'https://yoomoney.ru/api/incoming-transfer-accept'
10 |
11 | operation_id: str
12 | protection_code: str
13 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/build_auth_url.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import Any, ClassVar, List, cast
3 |
4 | from glQiwiApi.core.abc.api_method import APIMethod, Request, ReturningType
5 | from glQiwiApi.core.session.holder import HTTPResponse
6 |
7 | YOO_MONEY_LINK_REGEXP = re.compile(r'https?://yoomoney.ru/oauth2/authorize[?]requestid=\w+')
8 |
9 |
10 | class BuildAuthURL(APIMethod[str]):
11 | http_method: ClassVar[str] = 'POST'
12 | url: ClassVar[str] = 'https://yoomoney.ru/oauth/authorize'
13 |
14 | client_id: str
15 | scopes: List[str]
16 | redirect_uri: str
17 | response_type: str = 'code'
18 |
19 | def build_request(self, **url_format_kw: Any) -> 'Request':
20 | return Request(
21 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
22 | http_method=self.http_method,
23 | data={
24 | 'client_id': self.client_id,
25 | 'response_type': 'code',
26 | 'redirect_uri': self.redirect_uri,
27 | 'scope': ' '.join(self.scopes),
28 | },
29 | )
30 |
31 | @classmethod
32 | def parse_http_response(cls, response: HTTPResponse) -> ReturningType:
33 | try:
34 | return cast(
35 | str, re.findall(YOO_MONEY_LINK_REGEXP, response.body.decode('utf-8'))[0]
36 | ) # pragma: no cover
37 | except IndexError:
38 | raise Exception(
39 | 'Could not find the authorization link in the response from '
40 | 'the api, check the client_id value'
41 | )
42 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/get_access_token.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Optional
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod, ReturningType
4 | from glQiwiApi.core.session.holder import HTTPResponse
5 |
6 |
7 | class GetAccessToken(APIMethod[str]):
8 | http_method: ClassVar[str] = 'POST'
9 | url: ClassVar[str] = 'https://yoomoney.ru/oauth/token'
10 |
11 | code: str
12 | client_id: str
13 | grant_type: str = 'authorization_code'
14 | redirect_uri: str = 'https://example.com'
15 | client_secret: Optional[str] = None
16 |
17 | @classmethod
18 | def on_json_parse(cls, response: HTTPResponse) -> str:
19 | return response.json()['access_token']
20 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/make_cellular_payment.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import APIMethod
6 |
7 |
8 | class MakeCellularPayment(APIMethod[Dict[str, Any]]):
9 | http_method: ClassVar[str] = 'POST'
10 | url: ClassVar[str] = 'https://yoomoney.ru/api/operation-details'
11 |
12 | pattern_id: str
13 | phone_number: str = Field(..., alias='phone-number')
14 | amount: float
15 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/operation_details.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Union
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod
4 | from glQiwiApi.yoo_money.types import OperationDetails
5 |
6 |
7 | class OperationDetailsMethod(APIMethod[OperationDetails]):
8 | http_method: ClassVar[str] = 'POST'
9 | url: ClassVar[str] = 'https://yoomoney.ru/api/operation-details'
10 |
11 | operation_id: Union[int, str]
12 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/operation_history.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Any, ClassVar, Iterable, Optional, Union
3 |
4 | from pydantic import conint
5 |
6 | from glQiwiApi.core.abc.api_method import APIMethod, Request
7 | from glQiwiApi.utils.date_conversion import datetime_to_utc_in_iso_format
8 | from glQiwiApi.utils.payload import filter_dictionary_none_values
9 | from glQiwiApi.yoo_money.types import OperationHistory
10 |
11 | MAX_HISTORY_LIMIT = 100
12 |
13 |
14 | class OperationHistoryMethod(APIMethod[OperationHistory]):
15 | http_method: ClassVar[str] = 'GET'
16 | url: ClassVar[str] = 'https://yoomoney.ru/api/operation-history'
17 |
18 | operation_types: Optional[Iterable[str]] = None
19 | start_date: Optional[datetime] = None
20 | end_date: Optional[datetime] = None
21 | start_record: Optional[int] = None
22 | records: conint(le=MAX_HISTORY_LIMIT, strict=True, gt=0) = 30
23 | label: Optional[Union[str, int]] = None
24 | in_detail: bool = False
25 |
26 | def build_request(self, **url_format_kw: Any) -> 'Request':
27 | payload = {
28 | 'records': self.records,
29 | 'label': self.label,
30 | 'start_record': self.start_record,
31 | 'details': str(self.in_detail).lower(),
32 | }
33 |
34 | if self.operation_types is not None:
35 | payload['type'] = ' '.join([op_type.lower() for op_type in self.operation_types])
36 | if self.start_date:
37 | payload['from'] = datetime_to_utc_in_iso_format(self.start_date)
38 | if self.end_date:
39 | payload['till'] = datetime_to_utc_in_iso_format(self.end_date)
40 |
41 | return Request(
42 | endpoint=self.url.format(**url_format_kw, **self._get_runtime_path_values()),
43 | http_method=self.http_method,
44 | data=filter_dictionary_none_values(payload),
45 | )
46 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/process_payment.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod
4 | from glQiwiApi.yoo_money.types import Payment
5 |
6 |
7 | class ProcessPayment(APIMethod[Payment]):
8 | http_method: ClassVar[str] = 'POST'
9 | url: ClassVar[str] = 'https://yoomoney.ru/api/process-payment'
10 |
11 | request_id: str
12 | money_source: str = 'wallet'
13 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/reject_incoming_transfer.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Dict
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod
4 |
5 |
6 | class RejectIncomingTransfer(APIMethod[Dict[str, str]]):
7 | http_method: ClassVar[str] = 'POST'
8 | url: ClassVar[str] = 'https://yoomoney.ru/api/incoming-transfer-reject'
9 |
10 | operation_id: str
11 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/request_payment.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Optional, Union
2 |
3 | from pydantic import Field
4 |
5 | from glQiwiApi.core.abc.api_method import APIMethod
6 | from glQiwiApi.yoo_money.types import RequestPaymentResponse
7 |
8 |
9 | class RequestPayment(APIMethod[RequestPaymentResponse]):
10 | http_method: ClassVar[str] = 'POST'
11 | url: ClassVar[str] = 'https://yoomoney.ru/api/request-payment'
12 |
13 | to_account: str = Field(..., alias='to')
14 | amount: Union[int, float] = Field(..., alias='amount_due')
15 | pattern_id: str = 'p2p'
16 | comment_for_history: Optional[str] = Field(None, alias='comment')
17 | comment_for_receiver: Optional[str] = Field(None, alias='message')
18 | protect: bool = Field(False, alias='codepro')
19 | expire_period: int = 1
20 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/retrieve_account_info.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod
4 | from glQiwiApi.yoo_money.types import AccountInfo
5 |
6 |
7 | class RetrieveAccountInfo(APIMethod[AccountInfo]):
8 | http_method: ClassVar[str] = 'POST'
9 | url: ClassVar[str] = 'https://yoomoney.ru/api/account-info'
10 |
--------------------------------------------------------------------------------
/glQiwiApi/yoo_money/methods/revoke_api_token.py:
--------------------------------------------------------------------------------
1 | from typing import Any, ClassVar, Dict
2 |
3 | from glQiwiApi.core.abc.api_method import APIMethod, ReturningType
4 | from glQiwiApi.core.session.holder import HTTPResponse
5 |
6 |
7 | class RevokeAPIToken(APIMethod[Dict[Any, Any]]):
8 | http_method: ClassVar[str] = 'POST'
9 | url: ClassVar[str] = 'https://yoomoney.ru/api/revoke'
10 |
11 | @classmethod
12 | def parse_http_response(cls, response: HTTPResponse) -> ReturningType:
13 | return {}
14 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | import pytest
4 |
5 | from glQiwiApi.qiwi.clients.wallet.types import Transaction, TransactionStatus, TransactionWebhook
6 | from glQiwiApi.qiwi.clients.wallet.types.transaction import Provider, TransactionType
7 | from glQiwiApi.types.amount import AmountWithCurrency
8 |
9 |
10 | @pytest.fixture()
11 | def transaction() -> Transaction:
12 | return Transaction(
13 | txnId=50,
14 | personId=3254235,
15 | date=datetime.now(),
16 | status=TransactionStatus.SUCCESS,
17 | statusText='hello',
18 | trmTxnId='world',
19 | account='+38908234234',
20 | sum=AmountWithCurrency(amount=999, currency='643'),
21 | total=AmountWithCurrency(amount=999, currency='643'),
22 | provider=Provider(),
23 | commission=AmountWithCurrency(amount=999, currency='643'),
24 | currencyRate=643,
25 | type=TransactionType.OUT,
26 | comment='my_comment',
27 | )
28 |
29 |
30 | @pytest.fixture(name='test_webhook')
31 | def test_webhook_fixture():
32 | return TransactionWebhook.parse_obj(
33 | {
34 | 'messageId': 'dc32ba01-1a83-4dc5-82b5-1148f7744ace',
35 | 'hookId': '87995a67-749f-4a23-9629-95dc8fea696f',
36 | 'payment': {
37 | 'txnId': '22994210671',
38 | 'date': '2021-08-24T22:48:03+03:00',
39 | 'type': 'OUT',
40 | 'status': 'SUCCESS',
41 | 'errorCode': '0',
42 | 'personId': 380968317459,
43 | 'account': '+380985272064',
44 | 'comment': '',
45 | 'provider': 99,
46 | 'sum': {'amount': 1, 'currency': 643},
47 | 'commission': {'amount': 0.02, 'currency': 643},
48 | 'total': {'amount': 1.02, 'currency': 643},
49 | 'signFields': 'sum.currency,sum.amount,type,account,txnId',
50 | },
51 | 'hash': '1de3c14fbe8687b71b34ee23bd55262167304bb8f7d1bb8adaa5d8b4474fb148',
52 | 'version': '1.0.0',
53 | 'test': False,
54 | }
55 | )
56 |
--------------------------------------------------------------------------------
/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/integration/__init__.py
--------------------------------------------------------------------------------
/tests/integration/test_qiwi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/integration/test_qiwi/__init__.py
--------------------------------------------------------------------------------
/tests/integration/test_qiwi/test_clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/integration/test_qiwi/test_clients/__init__.py
--------------------------------------------------------------------------------
/tests/integration/test_qiwi/test_clients/test_maps.py:
--------------------------------------------------------------------------------
1 | from typing import Any, AsyncIterator, Dict
2 |
3 | import pytest
4 |
5 | from glQiwiApi import QiwiMaps
6 | from glQiwiApi.qiwi.clients.maps.types.polygon import Polygon
7 | from glQiwiApi.qiwi.clients.maps.types.terminal import Terminal
8 | from glQiwiApi.qiwi.clients.wallet.types import Partner
9 |
10 | pytestmark = pytest.mark.asyncio
11 |
12 |
13 | @pytest.fixture(name='data')
14 | def maps_data() -> AsyncIterator[Dict[str, Any]]:
15 | polygon = Polygon(latNW=55.690881, lngNW=37.386282, latSE=55.580184, lngSE=37.826078)
16 | yield {'polygon': polygon, 'zoom': 12, 'cache_terminals': True}
17 | del polygon
18 |
19 |
20 | @pytest.fixture(name='maps')
21 | async def maps_fixture() -> AsyncIterator[QiwiMaps]:
22 | """:class:`QiwiMaps` fixture"""
23 | async with QiwiMaps() as maps:
24 | yield maps
25 |
26 |
27 | async def test_terminals(maps: QiwiMaps, data: Dict[str, Any]) -> None:
28 | result = await maps.terminals(**data, include_partners=True)
29 | assert all(isinstance(t, Terminal) for t in result)
30 |
31 |
32 | async def test_partners(maps: QiwiMaps) -> None:
33 | result = await maps.partners()
34 | assert all(isinstance(p, Partner) for p in result)
35 |
--------------------------------------------------------------------------------
/tests/integration/test_qiwi/test_clients/test_p2p_client.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import uuid
3 | from typing import Any, AsyncIterator, Dict
4 |
5 | import pytest
6 |
7 | from glQiwiApi import QiwiP2PClient
8 | from glQiwiApi.qiwi.clients.p2p.types import Bill
9 | from tests.settings import QIWI_P2P_CREDENTIALS
10 |
11 | pytestmark = pytest.mark.asyncio
12 |
13 |
14 | @pytest.fixture(name='api')
15 | async def api_fixture() -> AsyncIterator[QiwiP2PClient]:
16 | async with QiwiP2PClient(**QIWI_P2P_CREDENTIALS) as p2p:
17 | yield p2p
18 |
19 |
20 | @pytest.mark.parametrize(
21 | 'payload',
22 | [
23 | {'amount': 1},
24 | {'amount': 1, 'comment': 'test_comment'},
25 | {
26 | 'amount': 1,
27 | 'comment': 'test_comment',
28 | 'expire_at': datetime.datetime.now() + datetime.timedelta(hours=5),
29 | },
30 | {
31 | 'amount': 1,
32 | 'comment': 'test_comment',
33 | 'expire_at': datetime.datetime.now() + datetime.timedelta(hours=5),
34 | 'bill_id': str(uuid.uuid4()),
35 | },
36 | ],
37 | )
38 | async def test_create_p2p_bill(api: QiwiP2PClient, payload: Dict[str, Any]) -> None:
39 | result = await api.create_p2p_bill(**payload)
40 | assert isinstance(result, Bill)
41 | assert payload['amount'] == result.amount.value
42 |
43 |
44 | async def test_check_p2p_bill_status(api: QiwiP2PClient) -> None:
45 | test_bill = await api.create_p2p_bill(amount=1)
46 | result = await api.get_bill_status(bill_id=test_bill.id)
47 | assert isinstance(result, str)
48 |
49 |
50 | async def test_get_bill_by_id(api: QiwiP2PClient) -> None:
51 | test_bill = await api.create_p2p_bill(amount=1)
52 | assert await api.get_bill_by_id(test_bill.id) == test_bill
53 |
54 |
55 | async def test_check_p2p_on_object(api: QiwiP2PClient) -> None:
56 | bill = await api.create_p2p_bill(amount=1)
57 | assert isinstance(bill, Bill)
58 | result = await api.check_if_bill_was_paid(bill)
59 |
60 | assert result is False
61 |
62 |
63 | async def test_reject_p2p_bill(api: QiwiP2PClient) -> None:
64 | b = await api.create_p2p_bill(amount=1)
65 | rejected_bill = await api.reject_p2p_bill(b.id)
66 | assert isinstance(rejected_bill, Bill)
67 |
68 |
69 | async def test_reject_bill_alias(api: QiwiP2PClient) -> None:
70 | b = await api.create_p2p_bill(amount=1)
71 | rejected_bill = await api.reject_bill(b)
72 | assert rejected_bill.status.value == 'REJECTED'
73 |
74 |
75 | async def test_check_bill_status_alias(api: QiwiP2PClient) -> None:
76 | b = await api.create_p2p_bill(amount=1)
77 | assert await api.check_if_bill_was_paid(b) is False
78 |
--------------------------------------------------------------------------------
/tests/integration/test_yoomoney/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/integration/test_yoomoney/__init__.py
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | QIWI_WALLET_CREDENTIALS = {
4 | 'phone_number': os.getenv('QIWI_PHONE_NUMBER'),
5 | 'api_access_token': os.getenv('QIWI_API_ACCESS_TOKEN'),
6 | }
7 |
8 | QIWI_P2P_CREDENTIALS = {'secret_p2p': os.getenv('QIWI_SECRET_P2P')}
9 |
10 | YOO_MONEY_CREDENTIALS = {'api_access_token': os.getenv('YOOMONEY_API_TOKEN')}
11 |
12 | YOO_MONEY_TEST_CLIENT_ID = os.getenv('YOOMONEY_TEST_CLIENT_ID')
13 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/__init__.py
--------------------------------------------------------------------------------
/tests/unit/base.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.mark.unit_test
5 | class UnitTestCase:
6 | pass
7 |
--------------------------------------------------------------------------------
/tests/unit/test_api_methods/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_api_methods/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_api_methods/test_api_method.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from glQiwiApi.core.abc.api_method import APIMethod, Request, RuntimeValue
6 | from glQiwiApi.core.session.holder import HTTPResponse
7 |
8 |
9 | class MyModel(BaseModel):
10 | f: str
11 |
12 |
13 | class SimpleAPIMethod(APIMethod[MyModel]):
14 | url: ClassVar[str] = 'https://hello.world'
15 | http_method: ClassVar[str] = 'GET'
16 |
17 |
18 | class APIMethodWithRequestSchema(APIMethod[MyModel]):
19 | http_method: ClassVar[str] = 'PUT'
20 | url: ClassVar[str] = 'https://hello.world/{id}'
21 |
22 | json_payload_schema = {'hello': {'world': {'nested': RuntimeValue()}}}
23 |
24 | id: int = Field(..., path_runtime_value=True)
25 | nested_field: str = Field(..., scheme_path='hello.world.nested')
26 |
27 |
28 | def test_parse_response_if_status_code_is_200() -> None:
29 | assert isinstance(
30 | SimpleAPIMethod.parse_http_response(
31 | HTTPResponse(status_code=200, body=b'{"f": "some_value"}', headers={}, content_type='')
32 | ),
33 | MyModel,
34 | )
35 |
36 |
37 | def test_build_request_of_simple_api_method() -> None:
38 | method = SimpleAPIMethod()
39 | assert method.build_request() == Request(
40 | endpoint='https://hello.world',
41 | )
42 |
43 |
44 | def test_build_complicated_request() -> None:
45 | method = APIMethodWithRequestSchema(id=5, nested_field='hello world')
46 | assert method.build_request() == Request(
47 | endpoint='https://hello.world/5',
48 | json_payload={'hello': {'world': {'nested': 'hello world'}}},
49 | http_method='PUT',
50 | )
51 |
52 |
53 | def test_determine_returning_type_by_generic_value() -> None:
54 | method = SimpleAPIMethod()
55 | assert method.__returning_type__ is MyModel
56 |
57 |
58 | def test_get_filled_request_schema() -> None:
59 | method = APIMethodWithRequestSchema(id=5, nested_field='hello world')
60 | assert method._get_filled_json_payload_schema() == {
61 | 'hello': {'world': {'nested': 'hello world'}}
62 | }
63 |
64 |
65 | def test_get_runtime_path_values() -> None:
66 | method = APIMethodWithRequestSchema(id=5, nested_field='hello world')
67 | assert method._get_runtime_path_values() == {'id': 5}
68 |
--------------------------------------------------------------------------------
/tests/unit/test_api_methods/test_qiwi_api_method.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import ClassVar, List
3 |
4 | import pytest
5 | from pydantic import BaseModel
6 |
7 | from glQiwiApi.core.abc.api_method import APIMethod
8 | from glQiwiApi.core.session.holder import HTTPResponse
9 | from glQiwiApi.qiwi.base import QiwiAPIMethod
10 | from glQiwiApi.qiwi.exceptions import QiwiAPIError
11 |
12 |
13 | class M(BaseModel):
14 | id: int
15 |
16 |
17 | class MyQiwiAPIMethod(QiwiAPIMethod[M]):
18 | url: ClassVar[str] = 'https://qiwi.com/hello/world'
19 | http_method: ClassVar[str] = 'GET'
20 |
21 |
22 | def test_parse_http_response() -> None:
23 | method = MyQiwiAPIMethod()
24 | with pytest.raises(QiwiAPIError) as exc_info:
25 | resp = HTTPResponse(
26 | status_code=400,
27 | body=json.dumps({'message': 'Something went wrong', 'code': 'QWRPC-303'}).encode(
28 | 'utf-8'
29 | ),
30 | headers={},
31 | content_type='',
32 | )
33 | method.parse_http_response(resp)
34 |
35 | assert exc_info.value.http_response == resp
36 | assert exc_info.value.error_code == '303'
37 |
38 |
39 | def test_designate__returning_type__attribute() -> None:
40 | method = MyQiwiAPIMethod()
41 | assert method.__returning_type__ is M
42 |
43 |
44 | def test_designate__returning_type_with_two_models() -> None:
45 | class Model1(QiwiAPIMethod[List[int]]):
46 | url: ClassVar[str] = 'https://qiwi.com/hello/world'
47 | http_method: ClassVar[str] = 'GET'
48 |
49 | class Model2(QiwiAPIMethod[M]):
50 | url: ClassVar[str] = 'https://qiwi.com/hello/world'
51 | http_method: ClassVar[str] = 'GET'
52 |
53 | assert Model1.__returning_type__ is List[int]
54 | assert Model2.__returning_type__ is M
55 |
56 |
57 | def test_designate__returning_type__with_raw_api_method() -> None:
58 | class K(BaseModel):
59 | pass
60 |
61 | class MyAPIMethod(APIMethod[K]):
62 | url: ClassVar[str] = 'https://qiwi.com/hello/world'
63 | http_method: ClassVar[str] = 'PUT'
64 |
65 | assert MyAPIMethod.__returning_type__ is K
66 |
--------------------------------------------------------------------------------
/tests/unit/test_api_methods/test_runtime_value.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import pytest
4 |
5 | from glQiwiApi.core.abc.api_method import RuntimeValue
6 |
7 |
8 | @pytest.mark.parametrize(
9 | 'rv,expected',
10 | [
11 | (RuntimeValue(default='hello'), True),
12 | (RuntimeValue(default_factory=lambda: 'world'), True),
13 | (RuntimeValue(), False),
14 | ],
15 | )
16 | def test_has_default(rv: RuntimeValue, expected: bool) -> None:
17 | assert rv.has_default() is expected
18 |
19 |
20 | @pytest.mark.parametrize(
21 | 'rv,expected',
22 | [
23 | (RuntimeValue(default='hello'), 'hello'),
24 | (RuntimeValue(default_factory=lambda: 'world'), 'world'),
25 | (RuntimeValue(), None),
26 | ],
27 | )
28 | def test_get_default(rv: RuntimeValue, expected: Any) -> None:
29 | assert rv.get_default() == expected
30 |
--------------------------------------------------------------------------------
/tests/unit/test_errors/test_yoomoney_errors.py:
--------------------------------------------------------------------------------
1 | from typing import Type
2 |
3 | import pytest
4 |
5 | from glQiwiApi.yoo_money.exceptions import (
6 | AccountBlockedError,
7 | AccountClosedError,
8 | AuthorizationRejectError,
9 | ContractNotFoundError,
10 | ExtActionRequiredError,
11 | IllegalParamError,
12 | InsufficientScopeError,
13 | InvalidGrantError,
14 | InvalidRequestError,
15 | LimitExceededError,
16 | MoneySourceNotAvailableError,
17 | NotEnoughFundsError,
18 | PayeeNotFoundError,
19 | PaymentRefusedError,
20 | TechnicalError,
21 | UnauthorizedClientError,
22 | YooMoneyError,
23 | YooMoneyErrorSchema,
24 | )
25 |
26 |
27 | @pytest.mark.parametrize(
28 | ['error_code', 'expected_exception_type'],
29 | [
30 | ['not_enough_funds', NotEnoughFundsError],
31 | ['illegal_param_date', IllegalParamError],
32 | ['invalid_request', InvalidRequestError],
33 | ['unauthorized_client', UnauthorizedClientError],
34 | ['invalid_grant', InvalidGrantError],
35 | ['illegal_param_type', IllegalParamError],
36 | ['some_err', TechnicalError],
37 | ['illegal_param_operation_id', IllegalParamError],
38 | ['not_enough_funds', NotEnoughFundsError],
39 | ['payment_refused', PaymentRefusedError],
40 | ['payee_not_found', PayeeNotFoundError],
41 | ['authorization_reject', AuthorizationRejectError],
42 | ['limit_exceeded', LimitExceededError],
43 | ['account_blocked', AccountBlockedError],
44 | ['account_closed', AccountClosedError],
45 | ['ext_action_required', ExtActionRequiredError],
46 | ['contract_not_found', ContractNotFoundError],
47 | ['money_source_not_available', MoneySourceNotAvailableError],
48 | ['insufficient_scope', InsufficientScopeError],
49 | ],
50 | )
51 | def test_if_error_class_equals_to_expected(
52 | error_code: str, expected_exception_type: Type[Exception]
53 | ):
54 | with pytest.raises(expected_exception_type) as exc_info:
55 | YooMoneyError.raise_most_appropriate_error(YooMoneyErrorSchema(error_code=error_code))
56 |
57 | assert exc_info.errisinstance(expected_exception_type)
58 |
59 |
60 | @pytest.mark.parametrize(
61 | ['expected_param_name', 'error_code'],
62 | [
63 | ['operation_id', 'illegal_param_operation_id'],
64 | ['date', 'illegal_param_date'],
65 | ],
66 | )
67 | def test_if_illegal_param_error_contains_param_name(expected_param_name: str, error_code: str):
68 | with pytest.raises(IllegalParamError) as exc_info:
69 | YooMoneyError.raise_most_appropriate_error(YooMoneyErrorSchema(error_code=error_code))
70 |
71 | assert exc_info.value.param_name == expected_param_name
72 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_event_fetching/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/mocks.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 |
3 | TEST_BASE64_WEBHOOK_KEY = 'JcyVhjHCvHQwufz+IHXolyqHgEc5MoayBfParl6Guoc='
4 |
5 |
6 | @dataclasses.dataclass()
7 | class WebhookTestData:
8 | bill_webhook_json: str
9 | base64_key_to_compare_hash: str
10 | transaction_webhook_json: str
11 |
12 |
13 | MOCK_BILL_WEBHOOK_RAW_DATA = """{
14 | "bill": {
15 | "siteId": "9hh4jb-00",
16 | "billId": "cc961e8d-d4d6-4f02-b737-2297e51fb48e",
17 | "amount": {
18 | "value": "1.00",
19 | "currency": "RUB"
20 | },
21 | "status": {
22 | "value": "PAID",
23 | "changedDateTime": "2021-01-18T15:25:18+03"
24 | },
25 | "customer": {
26 | "phone": "78710009999",
27 | "email": "test@tester.com",
28 | "account": "454678"
29 | },
30 | "customFields": {
31 | "paySourcesFilter": "qw",
32 | "themeCode": "Yvan-YKaSh",
33 | "yourParam1": "64728940",
34 | "yourParam2": "order 678"
35 | },
36 | "comment": "Text comment",
37 | "creationDateTime": "2021-01-18T15:24:53+03",
38 | "expirationDateTime": "2025-12-10T09:02:00+03"
39 | },
40 | "version": "1"
41 | }"""
42 |
43 | MOCK_TRANSACTION_WEBHOOK_RAW_DATA = """{
44 | "messageId":"7814c49d-2d29-4b14-b2dc-36b377c76156",
45 | "hookId":"5e2027d1-f5f3-4ad1-b409-058b8b8a8c22",
46 | "payment":{
47 | "txnId":"13353941550",
48 | "date":"2018-06-27T13:39:00+03:00",
49 | "type":"IN",
50 | "status":"SUCCESS",
51 | "errorCode":"0",
52 | "personId":78000008000,
53 | "account":"+79165238345",
54 | "comment":"",
55 | "provider":7,
56 | "sum":{
57 | "amount":1,
58 | "currency":643
59 | },
60 | "commission":{
61 | "amount":0,
62 | "currency":643
63 | },
64 | "total":{
65 | "amount":1,
66 | "currency":643
67 | },
68 | "signFields":"sum.currency,sum.amount,type,account,txnId"
69 | },
70 | "hash":"76687ffe5c516c793faa46fafba0994e7ca7a6d735966e0e0c0b65eaa43bdca0",
71 | "version":"1.0.0",
72 | "test":false
73 | }"""
74 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_filters/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_event_fetching/test_filters/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_filters/test_custom_filters.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import pytest
4 | from pytest_mock import MockerFixture
5 |
6 | from glQiwiApi.core import BaseFilter
7 | from glQiwiApi.core.event_fetching import filters
8 | from glQiwiApi.qiwi.clients.wallet.types import Transaction
9 |
10 | pytestmark = pytest.mark.asyncio
11 |
12 |
13 | class FirstCustomFilter(BaseFilter[Transaction]):
14 | async def check(self, update: Transaction) -> bool:
15 | return False
16 |
17 |
18 | class SecondCustomFilter(BaseFilter[Transaction]):
19 | async def check(self, update: Transaction) -> bool:
20 | return True
21 |
22 |
23 | def test_create_filters():
24 | first_filter = FirstCustomFilter()
25 | second_filter = SecondCustomFilter()
26 | assert all(isinstance(f, BaseFilter) for f in [first_filter, second_filter])
27 |
28 |
29 | async def test_and_chain_filters(mocker: MockerFixture):
30 | mock = mocker.Mock(spec=Transaction)
31 | first_filter = FirstCustomFilter()
32 | second_filter = SecondCustomFilter()
33 | chained_filter = filters.AndFilter(first_filter, second_filter)
34 | assert not await chained_filter.check(mock)
35 |
36 |
37 | async def test_not_filter(mocker: MockerFixture):
38 | mock = mocker.Mock(spec=Transaction)
39 | not_filter = filters.NotFilter(FirstCustomFilter())
40 | assert await not_filter.check(mock) is True
41 |
42 |
43 | @pytest.mark.xfail(
44 | raises=TypeError,
45 | reason="We cannot chain two filters, if they don't inherits from BaseFilter",
46 | )
47 | async def test_fail_chain_filters():
48 | first_filter = FirstCustomFilter()
49 | second_filter = lambda x: x is not None # wrong type # noqa
50 | filters.AndFilter(first_filter, second_filter) # type: ignore # noqa
51 |
52 |
53 | async def test_check_lambda_filter(mocker: MockerFixture):
54 | lambda_filter: filters.LambdaBasedFilter[Transaction] = filters.LambdaBasedFilter(
55 | lambda x: x is not None
56 | )
57 | mock = mocker.Mock(spec=Transaction)
58 | assert await lambda_filter.check(mock) is True
59 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_event_fetching/test_webhook/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tests.unit.test_event_fetching.mocks import (
4 | MOCK_BILL_WEBHOOK_RAW_DATA,
5 | MOCK_TRANSACTION_WEBHOOK_RAW_DATA,
6 | TEST_BASE64_WEBHOOK_KEY,
7 | WebhookTestData,
8 | )
9 |
10 |
11 | @pytest.fixture()
12 | def test_data() -> WebhookTestData:
13 | return WebhookTestData(
14 | bill_webhook_json=MOCK_BILL_WEBHOOK_RAW_DATA,
15 | base64_key_to_compare_hash=TEST_BASE64_WEBHOOK_KEY,
16 | transaction_webhook_json=MOCK_TRANSACTION_WEBHOOK_RAW_DATA,
17 | )
18 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_app.py:
--------------------------------------------------------------------------------
1 | from aiohttp import web
2 | from aiohttp.pytest_plugin import AiohttpClient
3 | from aiohttp.test_utils import TestClient
4 | from aiohttp.web_app import Application
5 | from aiohttp.web_request import Request
6 |
7 | from glQiwiApi.core.event_fetching import IPFilter, QiwiDispatcher
8 | from glQiwiApi.core.event_fetching.webhooks.app import configure_app
9 | from glQiwiApi.core.event_fetching.webhooks.config import EncryptionConfig, WebhookConfig
10 | from glQiwiApi.core.event_fetching.webhooks.middlewares.ip import ip_filter_middleware
11 | from tests.unit.test_event_fetching.mocks import WebhookTestData
12 |
13 |
14 | class TestAiohttpServer:
15 | def test_configure_app(self, test_data: WebhookTestData):
16 | app = Application()
17 |
18 | dp = QiwiDispatcher()
19 | configure_app(
20 | dp,
21 | app,
22 | WebhookConfig(
23 | encryption=EncryptionConfig(
24 | secret_p2p_key='', base64_encryption_key=test_data.base64_key_to_compare_hash
25 | )
26 | ),
27 | )
28 |
29 | assert len(app.router.routes()) == 2
30 |
31 | async def test_ip_middleware(self, aiohttp_client: AiohttpClient):
32 | app = Application()
33 | ip_filter = IPFilter.default()
34 | app.middlewares.append(ip_filter_middleware(ip_filter))
35 |
36 | async def handler(_: Request):
37 | return web.json_response({'ok': True})
38 |
39 | app.router.add_route('POST', '/webhook', handler)
40 | client: TestClient = await aiohttp_client(app)
41 |
42 | resp = await client.post('/webhook')
43 | assert resp.status == 401
44 |
45 | resp = await client.post('/webhook', headers={'X-Forwarded-For': '79.142.16.2'})
46 | assert resp.status == 200
47 |
48 | resp = await client.post(
49 | '/webhook', headers={'X-Forwarded-For': '79.142.16.2,91.213.51.238'}
50 | )
51 | assert resp.status == 200
52 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_event_fetching/test_webhook/test_services/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_services/test_collision_detector.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import pytest
4 |
5 | from glQiwiApi.core.event_fetching import (
6 | HashBasedCollisionDetector,
7 | UnexpectedCollision,
8 | UnhashableObjectError,
9 | )
10 | from glQiwiApi.qiwi.clients.wallet.types import TransactionWebhook
11 |
12 |
13 | class UnhashableTestClass:
14 | def __eq__(self, other):
15 | return False
16 |
17 |
18 | @pytest.fixture(name='detector')
19 | def collision_detector_fixture() -> HashBasedCollisionDetector:
20 | return HashBasedCollisionDetector()
21 |
22 |
23 | def test_add_processed_event(
24 | test_webhook: TransactionWebhook, detector: HashBasedCollisionDetector
25 | ):
26 | detector.remember_processed_object(test_webhook)
27 | assert hash(test_webhook) in detector.already_processed_object_hashes
28 |
29 |
30 | def test_collision_error_raise(
31 | test_webhook: TransactionWebhook, detector: HashBasedCollisionDetector
32 | ):
33 | detector.remember_processed_object(test_webhook)
34 | with pytest.raises(UnexpectedCollision):
35 | detector.remember_processed_object(test_webhook)
36 |
37 |
38 | def test_fail_if_object_is_unhashable(
39 | test_webhook: TransactionWebhook, detector: HashBasedCollisionDetector
40 | ):
41 | with pytest.raises(UnhashableObjectError):
42 | detector.remember_processed_object(UnhashableTestClass())
43 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_services/test_security.py:
--------------------------------------------------------------------------------
1 | from ipaddress import IPv4Address, IPv4Network
2 |
3 | import pytest
4 |
5 | from glQiwiApi.core.event_fetching.webhooks.services.security.ip import IPFilter
6 |
7 |
8 | class TestSecurity:
9 | def test_empty_init(self):
10 | ip_filter = IPFilter()
11 | assert not ip_filter._allowed_ips
12 |
13 | @pytest.mark.parametrize(
14 | 'ip,result',
15 | [
16 | ('127.0.0.1', True),
17 | ('127.0.0.2', False),
18 | (IPv4Address('127.0.0.1'), True),
19 | (IPv4Address('127.0.0.2'), False),
20 | (IPv4Address('91.213.51.3'), True),
21 | ('192.168.0.33', False),
22 | ('91.213.51.5', True),
23 | ('91.213.51.8', True),
24 | ('10.111.1.100', False),
25 | ],
26 | )
27 | def test_check_ip(self, ip, result):
28 | ip_filter = IPFilter(
29 | ips=['127.0.0.1', IPv4Address('91.213.51.3'), IPv4Network('91.213.51.0/24')]
30 | )
31 | assert (ip in ip_filter) is result
32 |
33 | def test_default(self):
34 | ip_filter = IPFilter.default()
35 | assert isinstance(ip_filter, IPFilter)
36 | assert len(ip_filter._allowed_ips) == 5880
37 | assert '79.142.16.1' in ip_filter
38 | assert '195.189.100.19' in ip_filter
39 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_utils.py:
--------------------------------------------------------------------------------
1 | from aiohttp import web
2 | from aiohttp.web_request import Request
3 |
4 | from glQiwiApi.core.event_fetching.webhooks.utils import inject_dependencies
5 |
6 |
7 | class A:
8 | pass
9 |
10 |
11 | class B:
12 | pass
13 |
14 |
15 | class SomeView(web.View):
16 | def __init__(self, request: Request, a: A, b: B):
17 | super().__init__(request)
18 | self.a = a
19 | self.b = b
20 |
21 | async def post(self):
22 | return web.json_response(data={'ok': True})
23 |
24 |
25 | def test_inject_dependencies():
26 | dependencies = {'a': A(), 'b': B()}
27 | patched_view = inject_dependencies(SomeView, dependencies)
28 | assert patched_view('hello').a == dependencies['a'] # type: ignore # noqa
29 | assert patched_view('hello').b == dependencies['b'] # type: ignore # noqa
30 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_event_fetching/test_webhook/test_views/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_views/conftest.py:
--------------------------------------------------------------------------------
1 | from typing import no_type_check
2 |
3 | import pytest
4 |
5 |
6 | @no_type_check
7 | @pytest.fixture
8 | def loop(event_loop):
9 | """
10 | Spike for compatibility pytest-asyncio and pytest-aiohttp
11 | @param event_loop:
12 | @return:
13 | """
14 | return event_loop
15 |
--------------------------------------------------------------------------------
/tests/unit/test_event_fetching/test_webhook/test_views/test_p2p_view.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from asyncio import AbstractEventLoop
3 |
4 | import pytest
5 | from aiohttp.pytest_plugin import AiohttpClient
6 | from aiohttp.test_utils import TestClient
7 | from aiohttp.web_app import Application
8 |
9 | from glQiwiApi.core import QiwiBillWebhookView
10 | from glQiwiApi.core.event_fetching import HashBasedCollisionDetector, QiwiDispatcher
11 | from glQiwiApi.core.event_fetching.webhooks.utils import inject_dependencies
12 | from glQiwiApi.qiwi.clients.p2p.types import BillWebhook
13 | from tests.unit.test_event_fetching.mocks import WebhookTestData
14 |
15 | pytestmark = pytest.mark.asyncio
16 |
17 |
18 | class QiwiBillWebhookViewWithoutSignatureValidation(QiwiBillWebhookView):
19 | def _validate_event_signature(self, update: BillWebhook) -> None:
20 | """
21 | We cannot test, because we have not X-Api-Signature-SHA256 for webhook.
22 | I have tested it manually on remote host
23 | """
24 |
25 |
26 | class TestBillWebhookView:
27 | async def test_with_right_payload(
28 | self, aiohttp_client: AiohttpClient, test_data: WebhookTestData, loop: AbstractEventLoop
29 | ):
30 | dp = QiwiDispatcher()
31 | app = Application()
32 | event_handled_by_handler = asyncio.Event()
33 |
34 | @dp.bill_handler()
35 | async def handle_bill_webhook(_: BillWebhook):
36 | event_handled_by_handler.set()
37 |
38 | app.router.add_view(
39 | handler=inject_dependencies(
40 | QiwiBillWebhookViewWithoutSignatureValidation,
41 | {
42 | 'event_cls': BillWebhook,
43 | 'dispatcher': dp,
44 | 'encryption_key': test_data.base64_key_to_compare_hash,
45 | 'collision_detector': HashBasedCollisionDetector(),
46 | },
47 | ),
48 | path='/webhook',
49 | name='bill_webhook',
50 | )
51 |
52 | client: TestClient = await aiohttp_client(app)
53 | response = await client.post('/webhook', json=test_data.bill_webhook_json)
54 |
55 | assert response.status == 200
56 | assert await response.json() == {'error': '0'}
57 |
58 | assert event_handled_by_handler.is_set() is True
59 |
--------------------------------------------------------------------------------
/tests/unit/test_initialization/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_initialization/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_initialization/test_qiwi_p2p_client.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | import pytest
4 |
5 | from glQiwiApi import QiwiP2PClient
6 | from glQiwiApi.qiwi.clients.p2p.client import NoShimUrlWasProvidedError
7 | from glQiwiApi.utils.compat import remove_suffix
8 |
9 |
10 | def test_old_style_deprecation_warn():
11 | old_style_shim_url = 'https://example.com/proxy/p2p/{0}'
12 |
13 | expected_warn_message = (
14 | 'Old-style urls that were used like format-like strings are deprecated '
15 | )
16 | f"use plain path like this - {remove_suffix(old_style_shim_url, '{0}')} instead."
17 |
18 | with pytest.warns(DeprecationWarning, match=expected_warn_message):
19 | _ = QiwiP2PClient(secret_p2p='fake secret p2p', shim_server_url=old_style_shim_url)
20 |
21 |
22 | def test_strip_format_variable_when_old_style_url_passed():
23 | old_style_shim_url = 'https://example.com/proxy/p2p/{0}'
24 |
25 | c = QiwiP2PClient(secret_p2p='fake secret p2p', shim_server_url=old_style_shim_url)
26 |
27 | assert c._shim_server_url == 'https://example.com/proxy/p2p/'
28 |
29 |
30 | def test_create_shim_url():
31 | c = QiwiP2PClient(
32 | secret_p2p='fake secret p2p', shim_server_url='https://example.com/proxy/p2p/'
33 | )
34 |
35 | random_uuid = str(uuid.uuid4())
36 |
37 | assert c.create_shim_url(random_uuid) == f'https://example.com/proxy/p2p/{random_uuid}'
38 |
39 |
40 | def test_create_shim_url_with_old_style_urls():
41 | c = QiwiP2PClient(
42 | secret_p2p='fake secret p2p', shim_server_url='https://example.com/proxy/p2p/{0}'
43 | )
44 |
45 | random_uuid = str(uuid.uuid4())
46 |
47 | assert c.create_shim_url(random_uuid) == f'https://example.com/proxy/p2p/{random_uuid}'
48 |
49 |
50 | def test_raise_when_shim_url_is_None():
51 | c = QiwiP2PClient(secret_p2p='fake secret p2p')
52 |
53 | with pytest.raises(NoShimUrlWasProvidedError):
54 | c.create_shim_url(str(uuid.uuid4()))
55 |
--------------------------------------------------------------------------------
/tests/unit/test_plugins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_plugins/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_plugins/test_telegram_plugins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_plugins/test_telegram_plugins/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_plugins/test_telegram_plugins/test_polling.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from aiogram import Bot, Dispatcher
3 | from pytest_mock import MockerFixture
4 |
5 | from glQiwiApi.plugins.aiogram.polling import AiogramPollingPlugin
6 |
7 |
8 | class TestPollingPlugin:
9 | @pytest.mark.skipif(
10 | 'sys.version_info <= (3, 8)',
11 | reason="required functionality of pytest-mock doesn't work for this test on python <= 3.8",
12 | )
13 | async def test_install_telegram_polling_plugin(self, mocker: MockerFixture):
14 | dispatcher = Dispatcher(Bot(token='231:23dfgd', validate_token=False))
15 | start_polling_mock_method = mocker.AsyncMock(spec=dispatcher.start_polling)
16 |
17 | dispatcher.start_polling = start_polling_mock_method
18 | plugin = AiogramPollingPlugin(dispatcher=dispatcher)
19 | await plugin.install(ctx={})
20 |
21 | start_polling_mock_method.assert_awaited_once()
22 |
23 | async def test_shutdown_polling_plugin(self, mocker: MockerFixture):
24 | dispatcher = Dispatcher(Bot(token='231:23dfgd', validate_token=False))
25 | shutdown_mock_method = mocker.Mock(spec=dispatcher.stop_polling)
26 | dispatcher.stop_polling = shutdown_mock_method
27 | plugin = AiogramPollingPlugin(dispatcher=dispatcher)
28 | await plugin.shutdown()
29 |
30 | shutdown_mock_method.assert_called_once()
31 |
--------------------------------------------------------------------------------
/tests/unit/test_plugins/test_telegram_plugins/test_webhook.py:
--------------------------------------------------------------------------------
1 | import py.path
2 | import pytest
3 | from aiogram import Bot, Dispatcher
4 | from pytest_mock import MockerFixture
5 |
6 | from glQiwiApi.core.event_fetching.webhooks.config import ApplicationConfig
7 | from glQiwiApi.plugins.aiogram.webhook import (
8 | DEFAULT_TELEGRAM_WEBHOOK_PATH,
9 | DEFAULT_TELEGRAM_WEBHOOK_PATH_PREFIX,
10 | AiogramWebhookPlugin,
11 | )
12 | from glQiwiApi.utils.certificates import SSLCertificate, get_or_generate_self_signed_certificate
13 |
14 |
15 | @pytest.fixture()
16 | def self_signed_certificate(tmpdir: py.path.local) -> SSLCertificate:
17 | tmpdir.mkdir('test_certificates')
18 | path_to_cert = tmpdir.join('cert.pem')
19 | path_to_pkey = tmpdir.join('pkey.pem')
20 | return get_or_generate_self_signed_certificate(
21 | hostname='45.138.24.80', cert_path=path_to_cert, pkey_path=path_to_pkey
22 | )
23 |
24 |
25 | class TestWebhookPlugins:
26 | @pytest.mark.skipif(
27 | 'sys.version_info <= (3, 8)',
28 | reason="required functionality of pytest-mock doesn't work for this test on python <= 3.8",
29 | )
30 | async def test_webhook_plugin_install(
31 | self, mocker: MockerFixture, self_signed_certificate: SSLCertificate
32 | ):
33 | bot = Bot('32423:dfgd', validate_token=False)
34 |
35 | async def set_webhook_stub(*args, **kwargs):
36 | pass
37 |
38 | bot.set_webhook = set_webhook_stub
39 | spy = mocker.spy(bot, 'set_webhook')
40 | dispatcher = Dispatcher(bot)
41 |
42 | plugin = AiogramWebhookPlugin(
43 | dispatcher,
44 | host='localhost',
45 | app_config=ApplicationConfig(ssl_certificate=self_signed_certificate),
46 | )
47 | run_app_mock = mocker.patch('glQiwiApi.plugins.aiogram.webhook.run_app', autospec=True)
48 | await plugin.install(ctx={})
49 |
50 | expected_webhook_url = (
51 | 'localhost' # noqa
52 | + DEFAULT_TELEGRAM_WEBHOOK_PATH_PREFIX # noqa
53 | + DEFAULT_TELEGRAM_WEBHOOK_PATH.format(token='32423:dfgd') # noqa
54 | )
55 | spy.assert_awaited_once_with(
56 | url=expected_webhook_url, certificate=self_signed_certificate.as_input_file()
57 | )
58 | run_app_mock.assert_called_once()
59 |
--------------------------------------------------------------------------------
/tests/unit/test_session/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/tests/unit/test_session/test_aiohttp_session_holder.py:
--------------------------------------------------------------------------------
1 | from typing import AsyncGenerator
2 |
3 | import aiohttp
4 | import pytest
5 | from aiohttp import TCPConnector
6 |
7 | from glQiwiApi.core.session.holder import AiohttpSessionHolder
8 |
9 | pytestmark = pytest.mark.asyncio
10 |
11 |
12 | @pytest.fixture()
13 | async def session_holder() -> AsyncGenerator[AiohttpSessionHolder, None]:
14 | holder = AiohttpSessionHolder()
15 | yield holder
16 | await holder.close()
17 |
18 |
19 | async def test_get_session(session_holder: AiohttpSessionHolder) -> None:
20 | session: aiohttp.ClientSession = await session_holder.get_session()
21 | assert isinstance(session, aiohttp.ClientSession) is True
22 |
23 |
24 | async def test_close(session_holder: AiohttpSessionHolder) -> None:
25 | await session_holder.get_session()
26 | await session_holder.close()
27 | assert session_holder._session.closed is True
28 |
29 |
30 | async def test_update_session_kwargs(session_holder: AiohttpSessionHolder) -> None:
31 | conn = TCPConnector()
32 | session_holder.update_session_kwargs(connector=conn)
33 | assert session_holder._session_kwargs.get('connector') == conn
34 |
35 |
36 | async def test_context_manager_of_holder(session_holder: AiohttpSessionHolder) -> None:
37 | async with session_holder as new_session:
38 | assert isinstance(new_session, aiohttp.ClientSession) is True
39 | assert new_session.closed is True
40 |
--------------------------------------------------------------------------------
/tests/unit/test_types/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/tests/unit/test_types/test_arbitrary/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
--------------------------------------------------------------------------------
/tests/unit/test_utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_utils/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_utils/test_api_helper.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from glQiwiApi import QiwiMaps
4 | from glQiwiApi.qiwi.clients.wallet.types import Partner
5 | from glQiwiApi.utils.synchronous import async_as_sync
6 |
7 |
8 | def test_async_as_sync():
9 | result = 0
10 |
11 | @async_as_sync()
12 | async def my_async_func():
13 | nonlocal result
14 | await asyncio.sleep(0.1)
15 | result += 1
16 |
17 | my_async_func()
18 | assert result == 1
19 |
20 |
21 | def test_async_as_sync_with_callback():
22 | callback_visited = asyncio.Event()
23 |
24 | @async_as_sync()
25 | async def callback():
26 | callback_visited.set()
27 |
28 | @async_as_sync(async_shutdown_callback=callback)
29 | async def my_async_func():
30 | async with QiwiMaps() as maps:
31 | partners = await maps.partners()
32 | assert all(isinstance(p, Partner) for p in partners)
33 |
34 | my_async_func()
35 | assert callback_visited.is_set() is True
36 |
--------------------------------------------------------------------------------
/tests/unit/test_utils/test_certificates.py:
--------------------------------------------------------------------------------
1 | import ssl
2 |
3 | from py.path import local
4 |
5 | from glQiwiApi.utils.certificates import get_or_generate_self_signed_certificate
6 |
7 |
8 | def test_generate_self_signed_certificates(tmpdir: local) -> None:
9 | tmpdir.mkdir('certificates')
10 | path_to_cert = tmpdir.join('cert.pem')
11 | path_to_pkey = tmpdir.join('pkey.pem')
12 | get_or_generate_self_signed_certificate(
13 | hostname='45.138.24.80', cert_path=path_to_cert, pkey_path=path_to_pkey
14 | )
15 | assert path_to_cert.isfile() is True
16 | assert path_to_pkey.isfile() is True
17 |
18 |
19 | def test_get_ssl_context(tmpdir: local) -> None:
20 | tmpdir.mkdir('certificates')
21 | ssl_certificate = get_or_generate_self_signed_certificate(
22 | hostname='45.138.24.80',
23 | cert_path=tmpdir.join('cert.pem'),
24 | pkey_path=tmpdir.join('pkey.pem'),
25 | )
26 | context = ssl_certificate.as_ssl_context()
27 | assert isinstance(context, ssl.SSLContext)
28 |
29 |
30 | def test_get_input_file(tmpdir: local) -> None:
31 | tmpdir.mkdir('certificates')
32 | ssl_certificate = get_or_generate_self_signed_certificate(
33 | hostname='45.138.24.80',
34 | cert_path=tmpdir.join('cert.pem'),
35 | pkey_path=tmpdir.join('pkey.pem'),
36 | )
37 | input_file = ssl_certificate.as_input_file()
38 | assert input_file.get_file().read() == tmpdir.join('cert.pem').read().encode('utf-8')
39 |
--------------------------------------------------------------------------------
/tests/unit/test_utils/test_currency_parser.py:
--------------------------------------------------------------------------------
1 | from glQiwiApi.types._currencies import described
2 | from glQiwiApi.types.amount import CurrencyModel
3 | from glQiwiApi.utils.currency_util import Currency
4 |
5 |
6 | def test_parse_described_currencies():
7 | condition = all(isinstance(Currency().get(key), CurrencyModel) for key in described.keys())
8 | assert condition
9 |
10 |
11 | def test_parse_non_existent_currency():
12 | assert Currency().get(currency_code='dsfgsgdsfg') is None
13 |
--------------------------------------------------------------------------------
/tests/unit/test_utils/test_executor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_utils/test_executor/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_utils/test_executor/test_webhook.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GLEF1X/glQiwiApi/3414f3b6640531d8409a3f18d82edb87919aee88/tests/unit/test_utils/test_executor/test_webhook.py
--------------------------------------------------------------------------------