diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9814107..b40e1b9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,10 +4,14 @@ on:
push:
branches:
- main
+ paths:
+ - '**.py'
pull_request:
types:
- opened
- synchronize
+ paths:
+ - '**.py'
schedule:
# cron every week on monday
- cron: "0 0 * * 1"
diff --git a/.gitignore b/.gitignore
index 8a9a77c..adc6931 100644
--- a/.gitignore
+++ b/.gitignore
@@ -163,4 +163,6 @@ cython_debug/
#.idea/
# User Defined
-test.py
\ No newline at end of file
+test.py
+*.lcov
+site
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e5a6142..a7041ed 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,21 +1,22 @@
repos:
- - repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.5.1
- hooks:
- - id: mypy
- additional_dependencies:
- - pydantic
- - repo: https://github.com/astral-sh/ruff-pre-commit
- # Ruff version.
- rev: v0.3.4
- hooks:
- # Run the formatter.
- - id: ruff-format
- # Run the linter.
- - id: ruff
- args: [--fix]
- repo: local
hooks:
+ - id: format
+ name: format
+ entry: make format
+ types: [python]
+ language: system
+ - id: lint
+ name: lint
+ entry: make lint
+ types: [python]
+ language: system
+ pass_filenames: false
+ - id: typecheck
+ name: typecheck
+ entry: make type-check
+ types: [python]
+ language: system
- id: secure
name: secure
entry: make secure
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 347a612..99c5a02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,36 @@
+## v2.0.0
+
+[GitHub release](https://github.com/mmzeynalli/integrify/releases/tag/v2.0.0)
+
+### What's Changed
+
+#### Fixes
+
+* Changed the whole structure and released a new version with better handling of requests and responses.
+
+## v1.0.3 (2024-10-07)
+
+[GitHub release](https://github.com/mmzeynalli/integrify/releases/tag/v1.0.3)
+
+### What's Changed
+
+#### Fixes
+
+* Replaced `StrEnum` with `str, Enum` to be python <3.11 friendly.
+
+## v1.0.1 (2024-09-08)
+
+### What's Changed
+
+#### Fixes
+
+* Updated version for PyPI
+
## v1.0.0 (2024-09-27)
+### What's Changed
+
+#### New integrations
+
* Added EPoint intergration
* Added EPoint documentation
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 0000000..6cfb13f
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,24 @@
+# This CITATION.cff file was generated with cffinit.
+# Visit https://bit.ly/cffinit to generate yours today!
+
+cff-version: 1.2.0
+title: Integrify
+message: >-
+ If you use this software, please cite it using the
+ metadata from this file.
+type: software
+authors:
+ - given-names: Miradil
+ family-names: Zeynalli
+ email: miradil.zeynalli@gmail.com
+ - given-names: Vahid
+ family-names: Hasanzada
+ email: vahidzhe@gmail.com
+repository-code: 'https://github.com/mmzeynalli/integrify'
+url: 'https://integrify.mmzeynalli.dev/'
+abstract: >-
+ Integrify is a request library that eases the API
+ integrations.
+keywords:
+ - integrify
+license: GPL-3.0-or-later
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..5902800
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1 @@
+Proyektə öz tövhənizi qatmaq üçün, zəhmət olmazsa, [bu dokumentasiyanı](http://integrify.mmzeynalli.dev/resources/contributing/) oxuyun.
diff --git a/Makefile b/Makefile
index d6c585b..d89e46d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,25 +1,118 @@
-ifdef OS
- PYTHON ?= .venv/Scripts/python.exe
- TYPE_CHECK_COMMAND ?= echo Pytype package doesn't support Windows OS
-else
- PYTHON ?= .venv/bin/python
- TYPE_CHECK_COMMAND ?= ${PYTHON} -m pytype --config=pytype.cfg src
-endif
+.PHONY: .poetry ## Check that Poetry is installed
+.poetry:
+ @poetry -V || echo 'Please install Poetry: https://python-poetry.org/docs/#installation'
-SETTINGS_FILENAME = pyproject.toml
+.PHONY: .pre-commit ## Check that pre-commit is installed
+.pre-commit:
+ @pre-commit -V || echo 'Please install pre-commit: https://pre-commit.com/'
-.PHONY: install
-install:
+.PHONY: install ## Install the package, dependencies, and pre-commit for local development
+install: .poetry
poetry install --no-interaction
.PHONY: install-main
-install-main:
+install-main: .poetry
poetry install --no-interaction --only main
+.PHONY: refresh-lockfiles ## Sync lockfiles with requirements files.
+refresh-lockfiles: .poetry
+ poetry lock --no-update
+
+.PHONY: rebuild-lockfiles ## Rebuild lockfiles from scratch, updating all dependencies
+rebuild-lockfiles: .poetry
+ poetry lock
+
+.PHONY: format ## Auto-format python source files
+format: .poetry
+ poetry run ruff check --fix
+ poetry run ruff format
+
+.PHONY: lint ## Lint python source files
+lint: .poetry
+ poetry run ruff check
+ poetry run ruff format --check
+
+.PHONY: type-check ## Type-check python source files
+type-check: .poetry
+ poetry run mypy .
+
+.PHONY: test ## Run all tests
+test: .poetry
+ poetry run coverage run -m pytest --durations=10
+
+.PHONY: testcov ## Run tests and generate a coverage report
+testcov: test
+ @echo "building coverage html"
+ @poetry run coverage html
+ @echo "building coverage lcov"
+ @poetry run coverage lcov
+
+.PHONY: testcov-badge ## Generate badge after tests
+testcov-badge:
+ @poetry run coverage-badge -o coverage.svg
+
+lang=az
+
+.PHONY: docs ## Generate the docs
+docs:
+ poetry run mkdocs build -f docs/${lang}/mkdocs.yml --strict
+
+.PHONY: docs-serve ## Serve the docs
+docs-serve:
+ poetry run mkdocs serve -f docs/${lang}/mkdocs.yml
+
.PHONY: secure
secure:
- poetry run bandit -r integrify --config ${SETTINGS_FILENAME}
+ poetry run bandit -r integrify --config pyproject.toml
+
+.PHONY: all ## Run the standard set of checks performed in CI
+all: lint testcov
+
+.PHONY: clean ## Clear local caches and build artifacts
+clean:
+ifeq ($(OS),Windows_NT)
+ del /s /q __pycache__
+ del /s /q *.pyc *.pyo
+ del /s /q *~ .*~
+ del /s /q .cache
+ del /s /q .pytest_cache
+ del /s /q .ruff_cache
+ del /s /q htmlcov
+ del /s /q *.egg-info
+ del /s /q .coverage .coverage.*
+ del /s /q build
+ del /s /q dist
+ del /s /q site
+ del /s /q docs\_build
+ del /s /q coverage.xml
+else
+ rm -rf `find . -name __pycache__`
+ rm -f `find . -type f -name '*.py[co]'`
+ rm -f `find . -type f -name '*~'`
+ rm -f `find . -type f -name '.*~'`
+ rm -rf .cache
+ rm -rf .pytest_cache
+ rm -rf .ruff_cache
+ rm -rf htmlcov
+ rm -rf *.egg-info
+ rm -f .coverage
+ rm -f .coverage.*
+ rm -rf build
+ rm -rf dist
+ rm -rf site
+ rm -rf docs/_build
+ rm -rf coverage.xml
+endif
+
+.PHONY: new-integration ## Create new integration folder
+new-integration:
+ mkdir src/integrify/${name}
+ touch src/integrify/${name}/__init__.py src/integrify/${name}/client.py src/integrify/${name}/handlers.py src/integrify/${name}/env.py
+ mkdir src/integrify/${name}/schemas
+ touch src/integrify/${name}/schemas/__init__.py src/integrify/${name}/schemas/request.py src/integrify/${name}/schemas/response.py;
+
+ mkdir tests/${name}
+ touch tests/${name}/__init__.py tests/${name}/conftest.py tests/${name}/mocks.py
-.PHONY: test
-test:
- poetry run pytest -s
\ No newline at end of file
+ mkdir docs/${lang}/docs/${name}
+ touch docs/${lang}/docs/${name}/about.md docs/${lang}/docs/${name}/api-reference.md
\ No newline at end of file
diff --git a/README.md b/README.md
index 4018b39..696b4cc 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,6 @@
-
---
**Dokumentasiya**: [https://integrify.mmzeynalli.dev](https://integrify.mmzeynalli.dev)
@@ -71,10 +70,10 @@ print(resp.ok, resp.body)
### Async
```python
-from integrify.epoint.asyncio import EPointRequest
+from integrify.epoint import EPointAsyncRequest
# Async main loop artıq başlamışdır
-resp = await EPointRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
+resp = await EPointAsyncRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
print(resp.ok, resp.body)
```
diff --git a/docs/assets/favicon.ico b/docs/az/docs/assets/favicon.ico
similarity index 100%
rename from docs/assets/favicon.ico
rename to docs/az/docs/assets/favicon.ico
diff --git a/docs/assets/integrify.png b/docs/az/docs/assets/integrify.png
similarity index 100%
rename from docs/assets/integrify.png
rename to docs/az/docs/assets/integrify.png
diff --git a/docs/assets/logo.svg b/docs/az/docs/assets/logo.svg
similarity index 100%
rename from docs/assets/logo.svg
rename to docs/az/docs/assets/logo.svg
diff --git a/docs/assets/spinner-solid.svg b/docs/az/docs/assets/spinner-solid.svg
similarity index 100%
rename from docs/assets/spinner-solid.svg
rename to docs/az/docs/assets/spinner-solid.svg
diff --git a/docs/css/custom.css b/docs/az/docs/css/custom.css
similarity index 100%
rename from docs/css/custom.css
rename to docs/az/docs/css/custom.css
diff --git a/docs/css/termynal.css b/docs/az/docs/css/termynal.css
similarity index 100%
rename from docs/css/termynal.css
rename to docs/az/docs/css/termynal.css
diff --git a/docs/epoint/about.md b/docs/az/docs/epoint/about.md
similarity index 97%
rename from docs/epoint/about.md
rename to docs/az/docs/epoint/about.md
index dd5b7c9..8877c45 100644
--- a/docs/epoint/about.md
+++ b/docs/az/docs/epoint/about.md
@@ -8,7 +8,7 @@
Sorğular uğurlu və ya uğursuz olduqda, spesifik URL-ə yönləndirmək istəyirsinizsə, bu dəyişənlərə də mühit levelində dəyər verin: `EPOINT_SUCCESS_REDIRECT_URL`, `EPOINT_FAILED_REDIRECT_URL`
-## Rəsmi Dokumentasiya (v1.0.3)
+## Rəsmi Dokumentasiya (v1.0.3) { #official-documentation }
[Azərbaycanca](https://epointbucket.s3.eu-central-1.amazonaws.com/files/instructions/API%20Epoint%20az.pdf)
@@ -16,7 +16,7 @@
[Rusca](https://epointbucket.s3.eu-central-1.amazonaws.com/files/instructions/API%20Epoint%20ru.pdf)
-## Sorğular listi
+## Sorğular listi { #list-of-requests }
| Sorğu metodu | Məqsəd | EPoint API | Callback-ə sorğu atılır |
| :-------------------------- | :------------------------------------------------------------------- | :---------------------------------------: | :-----------------------: |
@@ -31,7 +31,7 @@
| `split_pay_with_saved_card` | Saxlanılmış kartla ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə | `/api/1/split-execute-pay` | :x: |
| `split_pay_and_save_card` | Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə və kartı saxlamaq | `/api/1/split-card-registration-with-pay` | :fontawesome-solid-check: |
-## Callback Sorğusu
+## Callback Sorğusu { #callback-request }
Bəzi sorğular müştəri məlumat daxil etdikdən və arxa fonda bank işləmləri bitdikdən sonra, tranzaksiya haqqında məlumat sizin EPoint dashboard-da qeyd etdiyiniz `callback` URL-ə POST sorğusu göndərilir. Data siz adətən sorğu göndərdiyiniz formatda gəlir:
@@ -64,7 +64,7 @@ Bu data-nı `signature`-ni yoxladıqdan sonra, decode etmək lazımdır. Callbac
---
-## Callback Data formatı
+## Callback Data formatı { #callback-data-format }
Nə sorğu göndərməyinizdən asılı olaraq, callback-ə gələn data biraz fərqlənə bilər. `DecodedCallbackDataSchema` bütün bu dataları özündə cəmləsə də, hansı fieldlərin gəlməyəcəyini (yəni, decode-dan sonra `None` olacağını) bilmək yaxşı olar. Ümumilikdə, mümkün olacaq datalar bunlardır:
diff --git a/docs/epoint/api-reference.md b/docs/az/docs/epoint/api-reference.md
similarity index 64%
rename from docs/epoint/api-reference.md
rename to docs/az/docs/epoint/api-reference.md
index 26aa5bf..e3bf0f1 100644
--- a/docs/epoint/api-reference.md
+++ b/docs/az/docs/epoint/api-reference.md
@@ -1,20 +1,23 @@
+# EPoint klientinin API Reference-i
+
???+ note
İstifadəsi göstərilən bütün sorğular sinxrondur. Asinxron versiyasaları istifadə etmək üçün
bu importu edin və eyni-adlı funksiyaları `await` ilə çağırın:
```python
- from integrify.epoint.asyncio import EPointRequest
+ from integrify.epoint import EPointAsyncRequest
```
-::: integrify.epoint.sync.EPointRequest
+::: integrify.epoint.client.EPointRequest
+::: integrify.epoint.client.EPointAsyncRequest
???+ note
- Bu artıq hazır yaradılmış klass obyektidir, birbaşa istifadə üçün nəzərdə tutulub. Əks halda
+ Bunlar artıq hazır yaradılmış klass obyektləridir, birbaşa istifadə üçün nəzərdə tutulub. Əks halda
bütün sorğuları `EPointRequestClass().save_card()` kimi istifadə etməlisiniz.
-::: integrify.epoint.sync.EPointRequestClass
+::: integrify.epoint.client.EPointClientClass
handler: python
options:
members:
diff --git a/docs/index.md b/docs/az/docs/index.md
similarity index 62%
rename from docs/index.md
rename to docs/az/docs/index.md
index d46197e..28fb761 100644
--- a/docs/index.md
+++ b/docs/az/docs/index.md
@@ -34,7 +34,7 @@
---
-## Əsas özəlliklər
+## Əsas özəlliklər { #main-features }
- Kitabxana həm sync, həm də async sorğu dəyişimini dəstəkləyir.
- Kitabaxanadakı bütün sinif və funksiyalar tamamilə dokumentləşdirilib.
@@ -43,7 +43,7 @@
---
-## Kitabxananın yüklənməsi
+## Kitabxananın yüklənməsi { #installation }
@@ -53,7 +53,7 @@ $ pip install integrify
-## İstifadəsi
+## İstifadəsi { #usage }
Məsələn, EPoint üçün sorğuları istifadə etmək istərsək:
@@ -70,15 +70,15 @@ print(resp.ok, resp.body)
### Async
```python
-from integrify.epoint.asyncio import EPointRequest
+from integrify.epoint import EPointAsyncRequest
# Async main loop artıq başlamışdır
-resp = await EPointRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
+resp = await EPointAsyncRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
print(resp.ok, resp.body)
```
-### Sorğu cavabı
+### Sorğu cavabı { #request-response }
Yuxarıdakı sorğuların (və ya istənilən sorğunun) cavab formatı `ApiResponse` class-ıdır:
@@ -98,9 +98,9 @@ class ApiResponse:
```
-## Dəstəklənən API inteqrasiyaları
+## Dəstəklənən API inteqrasiyaları { #supported-integrations }
-| Servis | Əsas sorğular | Bütün sorğular | Dokumentləşdirilmə | Link |
-| ------- | :----------------------------------: | :----------------------------------: | ------------------ | -------------------------------------------------------------------------- |
-| EPoint | :heavy_check_mark: |  | Tam | [Docs](https://github.com/mmzeynalli/integrify/tree/main/integrify/epoint) |
-| Payriff |  |  | Tam |  |
+| Servis | Əsas sorğular | Bütün sorğular | Dokumentləşdirilmə | Link |
+| ------- | :---------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------: | ------------------ | ----------------------------------------------------------------------------------------------------- |
+| EPoint | :heavy_check_mark: |  | Tam | [Docs](https://github.com/mmzeynalli/integrify/tree/main/integrify/epoint) |
+| Payriff |  |  | Tam |  |
diff --git a/docs/js/custom.js b/docs/az/docs/js/custom.js
similarity index 100%
rename from docs/js/custom.js
rename to docs/az/docs/js/custom.js
diff --git a/docs/js/termynal.js b/docs/az/docs/js/termynal.js
similarity index 100%
rename from docs/js/termynal.js
rename to docs/az/docs/js/termynal.js
diff --git a/docs/az/docs/resources/api-reference.md b/docs/az/docs/resources/api-reference.md
new file mode 100644
index 0000000..7ec7579
--- /dev/null
+++ b/docs/az/docs/resources/api-reference.md
@@ -0,0 +1,44 @@
+# Daxili kod strukturunun API Reference-i
+
+## API
+
+::: integrify.api.APIClient
+ handler: python
+ options:
+ members:
+ - __init__
+ - add_url
+ - set_default_handler
+ - add_handler
+
+::: integrify.api.APIPayloadHandler
+ handler: python
+ options:
+ members:
+ - __init__
+ - headers
+ - pre_handle_payload
+ - handle_payload
+ - post_handle_payload
+ - handle_request
+ - handle_response
+
+::: integrify.api.APIExecutor
+ handler: python
+ options:
+ members:
+ - __init__
+ - request_function
+ - sync_req
+ - async_req
+
+## Schema
+
+::: integrify.schemas.APIResponse
+ handler: python
+
+::: integrify.schemas.PayloadBaseModel
+ handler: python
+ options:
+ members:
+ - from_args
diff --git a/docs/az/docs/resources/code-architecture.md b/docs/az/docs/resources/code-architecture.md
new file mode 100644
index 0000000..6c742a2
--- /dev/null
+++ b/docs/az/docs/resources/code-architecture.md
@@ -0,0 +1,233 @@
+# Kod arxitekturası
+
+## Bünovrə { #base }
+
+İlk öncə sorğunun hansı prosesdən keçdiyinə nəzər salaq. Nəzərə alaq ki, hal hazırda KapitalBank supportu olmasa da, əlavə edilib ki, iki fərqli sistem bir yerdə necə mövcud olacağı daha yaxşı göstərilsin.
+
+```mermaid
+graph TD;
+ classDef classAPI ;
+ classDef classHandler ;
+ classDef classExecutor ;
+
+ subgraph Request Execution
+ APIExecutor
+ httpx.Client
+ httpx.AsyncClient
+ end
+
+ subgraph API Handlers
+ APIPayloadHandler
+ epoint.PaymentPayloadHandler
+ kapital.PaymentPayloadHandler
+ end
+
+ subgraph API Support
+ APIClient
+ EPointClientClass
+ KapitalAPISupport
+ end
+
+ APIClient --> EPointClientClass
+ APIClient --> KapitalAPISupport
+ APIPayloadHandler --> epoint.PaymentPayloadHandler
+ APIPayloadHandler --> kapital.PaymentPayloadHandler
+ httpx.Client --> APIExecutor
+ httpx.AsyncClient --> APIExecutor
+
+ EPointClientClass -->|add_url & add_handler| APIClient
+ KapitalAPISupport -->|add_url & add_handler| APIClient
+ epoint.PaymentPayloadHandler -->|handle_request & handle_response| APIPayloadHandler
+ kapital.PaymentPayloadHandler -->|handle_request & handle_response| APIPayloadHandler
+ APIExecutor -->|sync_req| httpx.Client
+ APIExecutor -->|async_req| httpx.AsyncClient
+```
+
+## Axın { #code-flow }
+
+Bu strukturu nəzərdən keçisəniz, sorğunun hazırlanıb, göndərilib, cavabın parse və
+validate olunmasını bu diaqramdan anlaya bilərsiniz.
+
+```mermaid
+---
+title: EPointClient `pay` sorğusunun axını
+config:
+ theme: forest
+---
+sequenceDiagram;
+
+participant U as User
+participant C as EPointClientClass
+participant H as epoint.PaymentPayloadHandler
+participant E as APIExecutor
+
+U ->> C: .pay(data)
+Note over C: get_url
+
+C <<->> H: get_handler
+C <<->> E: get_request_function
+C ->> E: execute_request
+E <<->> H: handle_request (prepare data)
+Note over E: send request
+E <<->> H: handle_response
+E ->> C: return
+C ->> U: return
+```
+
+## Yeni inteqrasiya { #new-integration }
+
+Yeni inteqrasiya əlavə etmək istəyirsinizsə, zəhmət olmazsa [bu mərhələləri](./contributing.md) icra etdiyinizdən əmin olun. Kod yazmaq hissəyə gəldikdə isə, növbəti mərhələləri izləyin:
+
+### 0. File strukturu { #file-structure }
+
+Mövcud fayl strukturunu mimikləyə və ya sadəcə `make new-integration name=new_integration` kommandını icra edə bilərsiniz. Gözlənilən struktur budur:
+
+```text
+├── src
+| └── integrify
+| ├── __init__.py
+| ├── epoint
+| └── new_integration
+| ├── __init__.py
+| ├── client.py
+| ├── env.py
+| ├── handlers.py
+| └── schemas
+| ├── __init__.py
+| ├── request.py
+| └── response.py
+└── tests
+ ├── __init__.py
+ ├── epoint
+ └── new_integration
+ ├── __init__.py
+ ├── conftest.py
+ └── mocks.py
+```
+
+Əlavə fayllar (məsələn, `utils.py`, `helpers.py`), və qovluqlar əlavə etmək olar, bu struktur sadəcə bünovrə fayllar üçün nəzərdə tutulub.
+
+### 1. Hazırlıq və constant-lar { #preparation-and-constants }
+
+İlk öncə istifadə edəcəyiniz API-ləri (endpoint) bir enum constantları kimi yığın. Əgər mühit dəyişənləri oxunmalıdırsa, onu da `env.py` faylında edin. Başqa ümumi və mühitlik bir hissə varsa, bir fayl atında toplanmalıdır.
+
+### 2. Handler-lər { #handlers }
+
+Növbəti olaraq, handler-ləri yazmağınız məsləhət görülür. Hər bir sorğu üçün, göndərilən (`schemas/request.py`) və qəbul edilən (`schemas/response.py`) datanın schema-sını yazın.
+
+???+ warning
+
+ Request üçün nəzərdə tutulmuş schema-ların field-lərini user-dən aldığınız ardıcıllıqda yazın!
+ Yəni əgər sizin funksiyanız bu formatdadırsa:
+
+ ```python
+ def pay(amount: Decimal, currency: str)
+ ```
+
+ onda bu request üçün schema-dakı field-lərin ardıcıllığı:
+
+ ```python
+ class PaySchema(BaseModel):
+ amount: Decimal
+ currency: str
+ ```
+
+ olmalıdır. Əks halda, uinput data validation qarışa bilər, çünki, pozisional arqumentlərlər də işləyirik.
+ Daha ətraflı anlamaq üçün, [`PayloadBaseModel`][integrify.schemas.PayloadBaseModel]-ini araşdırın.
+
+Request və response handler-lər hazır olduqdan sonra, hər API üçün bir handler yazıb, bu yazdığınız request və response schema-larını orda qeyd edin.
+
+Nəzərə alın ki, əgər hər sorğu üçün, pre və ya post processing lazımdırsa, bunu schemalarda etmək əvəzinə, [`pre_handle_payload`][integrify.api.APIPayloadHandler.pre_handle_payload] və [`post_handle_payload`][integrify.api.APIPayloadHandler.post_handle_payload] funksiyalarını override etməklə eyni nəticəni əldə edə bilərsiniz. Misal üçün, EPoint-in handler-lərini nəzərdən keçirin.
+
+### 3. API Klient { #api-client }
+
+Bütün handler-lər hazır olduqdan sonra, yeni APIClient class-ı yaradıb, hər şeyi register etməlisiniz. Bir inteqrasiya üçün bir endpoint belə register olunur:
+
+```python
+class NewIntegrationClientClass(APIClient):
+
+ def __init__(self, sync: bool = True):
+ super().__init__('NewIntegration', 'https://new-integration.com', None, sync)
+
+ self.add_url('function1', env.API.API1, 'GET')
+ self.add_handler('function1', API1PayloadHandler)
+```
+
+Əgər API-lərin çoxu eyni handler ilə idarə oluna bilirsə, onda `default_handler` arqumentini `None` əvəzinə, o handler-i qeyd edə bilərsiniz. Yəni, kod əgər spesifik API üçün handler-i tapmazsa, default handler-i istifadə edəcək.
+
+???+ note
+
+ Məsləhət görülür ki, faylın sonunda sync və async clientləri obyekt kimi yaradıb, onları `new_integration/__init__.py`
+ faylına import edəsiniz. Bu user-in istifadəsini asanlaşdırır; hər dəfə class-ı yaratmaq yerinə hazır sizin obyekti
+ istifadə edir.
+
+### 4. Type-hinted funksiyalar { #type-hinted-functions }
+
+Bu hissənin kodun işləməsinə heç bir təsiri olmasa da, ən vacib hissələrdən biri hesab olunur. Nəzərə alın ki, bizim API klientlərin heç biri funksiya implement etmir; əvəzinə `__getattribute__` dunder metodu funksiyanın adından istifadə edərək, lazımı API və handler-ləri tapır. Ona görə də `function1` adlandırılması vacibdir, çünki mövcud olmayan funksiyanın adıdır.
+
+Konstruktorda bütün API və handlerlər register olunduqdan sonra, `if TYPE_CHECKING:` şərti altında funksiyalar yazılmalıdır. Funksiyaların bütün arqumentləri type-hint-lənməli və funksiyanın özünün docstring-i olmağı şərtdir, dokumentasiya generate edəndə buradan "oxuyur".
+
+Misal kod parçası:
+
+`````python
+class NewIntegrationClientClass(APIClient):
+
+ def __init__(self, sync: bool = True):
+ super().__init__('NewIntegration', 'https://new-integration.com', None, sync)
+
+ self.add_url('function1', env.API.API1, 'GET')
+ self.add_handler('function1', API1PayloadHandler)
+
+ if TYPE_CHECKING:
+
+ def function1(
+ self,
+ amount: Decimal,
+ currency: str,
+ ) -> APIResponse[API1ResponseSchema]:
+ """API sorğusu
+
+ **Endpoint:** */api/function-1*
+
+ Example:
+ ```python
+ from integrify.new_integration import NewIntegrationRequest
+
+ NewIntegrationRequest.function1(amount=100, currency='AZN')
+ ```
+
+ **Cavab formatı**: `API1PayloadHandler`
+
+ Bu sorğunu haqqında məlumat.
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Məzənnə.
+ """
+
+NewIntegrationRequest = NewIntegrationClientClass(sync=True)
+NewIntegrationAsyncRequest = NewIntegrationClientClass(sync=False)
+`````
+
+### 5. Testlər { #tests }
+
+Testsiz, əlbəttə ki, olmaz) Test üçün, `pytest` istifadə olunur. `conftest.py` faylında, əsas fixture-ləri əlavə edin. `mocks.py` faylı isə, mock response fixture-ləri üçün nəzərdə tutulub. Ondan sonra, testlər üçün fayllar yaradıb, orada testlərinizi yazın. Test coverage-dən istifadə edərək, nəzərdən qaçırdığınız hissələri də görə bilərsiniz.
+
+### 6. Dokumentasiya { #documentation }
+
+Kodun development-ini və testini bitirdikdən sonra, dokumentasiya yazmaq lazım olur. İlk mərhələdəki əməliyyatı icra etmişdinizsə, sizin üçün avtomatik markdown faylları da yaradılmışdır. `about.md` faylında inteqrasiya haqqında ümumi məlumat, orijinal dokumentasiyanın linklərini paylaşmağınız məsləhət görülür. Funksionallıqların və dəstəyini yazdığınız endpointləri də qeyd edin. Kodunuz bütün API-ləri istifadə etməyə bilər; bu halda, sadəcə bunu dokumentasiyada bilidirin.
+
+`api-reference.md` faylında isə, API reference-i generasiya edəcəksiniz. Bu formatı izləməlisiniz:
+
+```markdown
+::: integrify.new_integration.client.NewIntegrationClientClass
+ handler: python
+ options:
+ members:
+ - function1
+ - function2
+```
+
+Docstring-lər yazmısınızsa, [mkdocstrings](https://mkdocstrings.github.io/) aləti özü hər şeyi generasiya edəcəkdir.
+
+Dokumentasiyanı bitirdikdən sonra, `make docs` kommandası ilə dokumentasiyanın səhvsiz generasiya edildiyindən əmin olun. Dokumentasiyanı görmək üçün `make docs-serve` edin, və console-da qeyd olunan addressi browser-də açın (adətən `localhost:8000` olur).
diff --git a/docs/az/docs/resources/contributing.md b/docs/az/docs/resources/contributing.md
new file mode 100644
index 0000000..a2d72c3
--- /dev/null
+++ b/docs/az/docs/resources/contributing.md
@@ -0,0 +1,120 @@
+# Development - Contributing
+
+İlk öncə (və ən rahat), proyekti ulduzlayaraq yardımçı ola bilərsiniz.
+
+## Issue
+
+[Githubda](https://github.com/mmzeynalli/integrify/issues) yeni issue açaraq, yeni feature təklif edə, və ya mövcud bugları qeyd edə bilərsiniz.
+Bug qeyd etdikdə, istifadə etdiyiniz əməliyyat sistemi və kitabxananın versiyasını qeyd etməyiniz tövsiyyə olunur.
+
+## Development
+
+Əgər [integrify repo-sunu](https://github.com/mmzeynalli/integrify/) artıq klonlamısınızsa, növbəti mərhələləri izləyərək öz kodunuzu əlavə edə bilərsiniz. Contribution sadə və sürətli olsun deyə test və linting-i lokal mühitinizdə icra etməyiniz məsləhət görülür. Integrify-ın başqa kitabxanalardan çox az asılılığı olduğundan quraşdırılma çox sadədir.
+
+!!! tip
+
+ **tl;dr:** Kodu format etmək üçün `make format`, test və lint etmək üçün `make` və dokumentasiya generasiya etmək üçün `make docs` kommandını icra edin.
+
+### Rekvizitlər { #requisites }
+
+* Python 3.9 və 3.12 arası istənilən versiya
+* git
+* make
+* [Poetry](https://python-poetry.org/docs/#installation)
+
+### İnstallasiya və quraşdırılma { #installation }
+
+```bash
+# Proyekti klonlayın və həmin qovluğa keçin
+git clone git@github.com:/integrify.git
+cd integrify
+
+# Poetry yükləyin (https://python-poetry.org/docs/#installation)
+curl -sSL https://install.python-poetry.org | python3 -
+
+# Bütün dependency-ləri yükləyin
+make install
+```
+
+### Yeni branch-a keçin və öz dəyişiklikləriniz əlavə edin { #new-branch }
+
+```bash
+# Yeni branch-a keçid edin
+git checkout -b my-new-feature-branch
+# Öz dəyişikliklərini əlavə edin...
+```
+
+???+ warning
+
+ Branch adını uyğun seçin:
+
+ * Bug düzəldirsizsə, `bug/branch-name`
+ * Yeni inteqrasiya əlavə edirsizsə, `integration/integration-name`
+ * Kiçik fix-dirsə: `fix/branch-name`
+ * Dokumentasiya üzərində işləyirsinizsə: `docs/branch-name`
+
+### Test və Linting
+
+Kod dəyişiklikləriniz etdikdən sonra lokalda testləri və lintingi işə salın:
+
+```bash
+make format
+# Integrify Rust-da yazılmış ruff Python linterini istifadə edir
+# https://github.com/astral-sh/ruff
+
+make
+# Bu kommand öz içində bir neçə başqa kommandı icra edir (`test`, `testcov` and `lint`)
+```
+
+### Yeni dokumentasiyanı generasiya edin
+
+Əgər dokumentasiyada (və ya funksiyalarda, klass definitionlarında və ya docstring-lərdə) dəyişiklik etmisinizsə, yeni dokumentasiya generasiya edin.
+Dokumentasiya üçün `mkdocs-material` alətindən istifadə edirik.
+
+```bash
+# Dokumentasiya generasiya edin
+make docs
+# Əgər dokumentasiyaya təsir edəcək kod dəyişikliyi etmisinizsə,
+# əmin olun ki, yeni dokumentasiya uğurlar generasiya olunur.
+
+# make docs-serve kommandını icra etsəz, localhost:8000 addresində yeni dokumentasiyanı da görə bilərsiniz.
+```
+
+### Dəyişiklikləriniz commit və push edin { #commit-push-and-pr }
+
+Dəyişikliklərinizi bitirdək sonra, commit və öz branch-ınıza push edib, bizə pull request yaradın.
+
+Pull request-iniz review üçün hazırdırsa, "Zəhmət olmazsa, review edin" comment-ini yazın, ən yaxın zamanda nəzər yetirəcəyik.
+
+## Kod arxitekturası (!) { #code-architecture }
+
+Bu hissə uzun və detallı yazılmalı olduğundan, məqalə [burada](./code-architecture.md) yerləşdirilib.
+
+## Dokumentasiya stili { #documentation }
+
+Dokumentasiya markdown-da yazılır və [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) aləti ilə generasiya olunur. API dokumentasiyası isə docstring-lərdən [mkdocstrings](https://mkdocstrings.github.io/) ilə generasiya olunur.
+
+### Kodun dokumentləşdirilməsi { #incode-documentation-style }
+
+Öz dəyişikliklərinizi əlavə edərkən, bütün kodun dokumentləşdirildiyindən əmin olun. Qeyd olunanlar format olunmuş docstring-lərlə yaxşıca dokumentləşdirilməlidir:
+
+* Modullar
+* Klass definition-ları
+* Funksiya definition-ları
+* Module səviyyəsində dəyişənlər
+
+Integrify [PEP 257](https://www.python.org/dev/peps/pep-0257/) standartları ilə format olunmuş [Google-style docstring-lərdən](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) istifadə edir. (Əlavə məlumat üçün [Example Google Style Python Docstrings-ə](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) baxın.)
+
+Docstring-lərdə misal (example) göstərə bilərsiniz. Bu misal tam işlənə bilən kod olmalıdır.
+
+### Dokumentasiya { #documentation-style }
+
+Ümumiyyətlə, dokumentasiya əlçatan üslubda yazılmalıdır. Oxunması və başa düşülməsi asan olmalı, qısa və konkret olmalıdır.
+
+Kod nümunələri əlavə etməyiniz şiddətlə tövsiyyə olunur, lakin qısa və sadə saxlanılmalıdır. Bununla belə, hər bir kod nümunəsi tam, müstəqil və işlək olmalıdır. (Bunu necə edəcəyinizə əmin deyilsinizsə, kömək istəyin!).
+
+## Tərcümə { #translation }
+
+Hal-hazırda proyekt əsasən Azərbaycanlı developerlər üçün nəzərdə tutulduğundan, dokumentasiya və docstring-lər Azərbaycan dilindədir.
+Amma, bu proyekti gələcəkdə daha qloballaşdırmaq fikrində olduğumuzdan, ingiliscəyə tərcümədə yardıma ehtiyacımız var. Gələcəkdə ölkə-spesifik inteqrasiyalar
+mövcud olduqda, həmin dillərə də ehtiyac duyacağıq.
diff --git a/mkdocs.yml b/docs/az/mkdocs.yml
similarity index 90%
rename from mkdocs.yml
rename to docs/az/mkdocs.yml
index 0673ce0..d9d0b24 100644
--- a/mkdocs.yml
+++ b/docs/az/mkdocs.yml
@@ -4,6 +4,7 @@ site_url: https://integrify.mmzeynalli.dev
theme:
name: material
+ custom_dir: overrides
features:
- content.code.annotate
- content.code.copy
@@ -47,11 +48,13 @@ repo_url: https://github.com/mmzeynalli/integrify
plugins:
- search
+ - panzoom:
+ full_screen: True
- mkdocstrings:
default_handler: python
handlers:
python:
- paths: [src]
+ paths: [../../src]
options:
show_root_heading: true
show_source: false
@@ -74,6 +77,10 @@ nav:
- EPoint:
- Ümumi məlumat: "epoint/about.md"
- API Referansı: "epoint/api-reference.md"
+ - Resurslar:
+ - Contibuting: "resources/contributing.md"
+ - Kod Arxitekturası: "resources/code-architecture.md"
+ - API Reference: "resources/api-reference.md"
markdown_extensions:
- codehilite:
@@ -112,12 +119,13 @@ extra:
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/mmzeynalli/integrify
+ analytics:
+ provider: goatcounter
extra_css:
- css/termynal.css
- css/custom.css
extra_javascript:
- - js/termynal.js
- js/custom.js
- - js/goatcounter.js
\ No newline at end of file
+ - js/termynal.js
diff --git a/docs/az/overrides/partials/integrations/analytics/goatcounter.html b/docs/az/overrides/partials/integrations/analytics/goatcounter.html
new file mode 100644
index 0000000..cbd29ab
--- /dev/null
+++ b/docs/az/overrides/partials/integrations/analytics/goatcounter.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/js/goatcounter.js b/docs/js/goatcounter.js
deleted file mode 100644
index 8453805..0000000
--- a/docs/js/goatcounter.js
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/netlify.toml b/netlify.toml
index 69b4ff5..4df8ae6 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -1,6 +1,7 @@
[build]
+base = "docs/az"
publish = "site"
command = """
-pip3 install mkdocs-material mkdocstrings[python] &&
-mkdocs build -d site
+pip3 install mkdocs-material mkdocstrings[python] mkdocs-panzoom-plugin &&
+mkdocs build --strict
"""
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index cc6c82e..7c3d291 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "integrify"
packages = [{include = "integrify", from="src"}]
-version = "1.0.3"
+version = "2.0.0"
description = "Integrify API inteqrasiyalarını rahatlaşdıran sorğular kitabaxanasıdır."
license = "GPL-3.0-or-later"
authors = [
@@ -65,11 +65,18 @@ pre-commit = [
{ version = "^3.5.0", python = "3.8" }
]
ptpython = "^3.0.29"
-mkdocs-material = "^9.5.36"
-mkdocstrings = {extras = ["python"], version = "^0.26.1"}
bandit = "^1.7.10"
+coverage = [
+ { version = "^7.6.3", python = ">=3.9" },
+ { version = "7.6.1", python = "3.8" }
+]
coverage-badge = "^1.1.2"
+[tool.poetry.group.docs.dependencies]
+mkdocs-material = "^9.5.36"
+mkdocstrings = {extras = ["python"], version = "^0.26.1"}
+mkdocs-panzoom-plugin = "^0.1.3"
+
[tool.ruff]
target-version = "py38"
line-length = 100
@@ -82,7 +89,9 @@ select = [
"E",
"W",
# isort
- "I"
+ "I",
+ # Print statements
+ "T201"
]
ignore-init-module-imports = true
@@ -94,6 +103,19 @@ docstring-quotes = "double"
[tool.pytest.ini_options]
pythonpath = ["src/"]
+filterwarnings = [
+ "ignore::UserWarning",
+]
+
+[tool.coverage.report]
+exclude_also = [
+ "def __repr__",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "if TYPE_CHECKING:",
+ "class .*\\bProtocol\\):",
+ "@(abc\\.)?abstractmethod",
+]
[tool.bandit]
skips = []
diff --git a/src/integrify/api.py b/src/integrify/api.py
new file mode 100644
index 0000000..be57f54
--- /dev/null
+++ b/src/integrify/api.py
@@ -0,0 +1,261 @@
+from typing import Any, Callable, Coroutine, Optional, Type, Union
+from urllib.parse import urljoin
+
+import httpx
+
+from integrify.logger import LOGGER_FUNCTION
+from integrify.schemas import APIResponse, PayloadBaseModel, ResponseType
+
+
+class APIClient:
+ """
+ API inteqrasiyaları üçün klient
+ """
+
+ def __init__(
+ self,
+ name: str,
+ base_url: str,
+ default_handler: Optional['APIPayloadHandler'] = None,
+ sync: bool = True,
+ ):
+ """
+ Args:
+ name: Klient adı. Logging üçün istifadə olunur.
+ base_url: API-lərin əsas (kök) url-i
+ default_handler: default API handler. Bu handler əgər hər hansı bir API-yə
+ handler register olunmadıqda istifadə olunur.
+ sync: Sync (True) və ya Async (False) klient seçimi. Default olaraq sync seçilir.
+ """
+ self.base_url = base_url
+ self.default_handler = default_handler or None
+
+ self.request_executor = APIExecutor(name=name, sync=sync)
+ """API sorğularını icra edən obyekt"""
+
+ self.urls: dict[str, dict[str, str]] = {}
+ """API sorğularının endpoint və metodunun mapping-i"""
+
+ self.handlers: dict[str, APIPayloadHandler] = {}
+ """API sorğularının payload (request və response) handler-lərının mapping-i"""
+
+ def add_url(self, route_name: str, url: str, verb: str):
+ """Yeni endpoint əlavə etmə funksiyası
+
+ Args:
+ route_name: Funksionallığın adı (məs., `pay`, `refund` və s.)
+ url: Endpoint url-i
+ verb: Endpoint metodunun (`POST`, `GET`, və s.)
+ """
+ self.urls[route_name] = {'url': url, 'verb': verb}
+
+ def set_default_handler(self, handler_class: Type['APIPayloadHandler']):
+ """Sorğulara default handler setter-i
+
+ Args:
+ handler_class: Default handler class-ı
+ """
+ self.default_handler = handler_class()
+
+ def add_handler(self, route_name: str, handler_class: Type['APIPayloadHandler']):
+ """Endpoint-ə handler əlavə etmək method-u
+
+ Args:
+ route_name: Funksionallığın adı (məs., `pay`, `refund` və s.)
+ handler_class: Həmin sorğunun (və response-unun) payload handler class-ı
+ """
+ self.handlers[route_name] = handler_class()
+
+ def __getattribute__(self, name: str) -> Any:
+ """Möcüzənin baş verdiyi yer:
+
+ Bu kitanxanada, heç bir inteqrasiya üçün birbaşa funksiya mövcud deyil. Bunun yerinə,
+ bu dunder metodundan istifadə edərək, hansı endpointə nə sorğu atılacağını anlaya bilirik.
+ """
+ try:
+ return super().__getattribute__(name)
+ except AttributeError:
+ # Əgər "axtarılan" funksiyanın adı `self.urls` listimizdə mövcud deyilsə,
+ # exception qaldırırıq
+ if name not in self.urls:
+ raise
+
+ # "Axtarılan" funksiyanın adından istifadə edərək, lazımi endpoint, metod və handler-i
+ # taparaq, sorğunu icra edirik.
+ url = urljoin(self.base_url, self.urls[name]['url'])
+ verb = self.urls[name]['verb']
+ handler = self.handlers.get(name, self.default_handler)
+
+ func = self.request_executor.request_function
+ return lambda *args, **kwds: func(url, verb, handler, *args, **kwds)
+
+
+class APIPayloadHandler:
+ """Sorğu və cavab data payload-ları üçün handler class-ı"""
+
+ def __init__(
+ self,
+ req_model: Optional[Type[PayloadBaseModel]] = None,
+ resp_model: Optional[Type[ResponseType]] = None,
+ ):
+ """
+ Args:
+ req_model: Sorğunun payload model-i
+ resp_model: Sorğunun cavabının payload model-i
+ """
+ self.req_model = req_model
+ self.resp_model = resp_model
+
+ @property
+ def headers(self):
+ """Sorğunun header-ləri"""
+ return {}
+
+ def pre_handle_payload(self, *args, **kwds):
+ """Sorğunun payload-ının pre-processing-i. Əgər istənilən payload-a
+ əlavə datanı lazımdırsa (bütün sorğularda eyni olan data), bu funksiyadan
+ istifadə edə bilərsiniz.
+
+ Misal üçün: Bax [`EPointClientClass`][integrify.epoint.client.EPointClientClass]
+ """
+ pass # pragma: no cover
+
+ def handle_payload(self, *args, **kwds):
+ """Verilən argumentləri `self.req_model` formatında payload-a çevirən funksiya.
+ `self.req_model` qeyd edilməyibsə, bu funksiya override olunmalıdır (!).
+ """
+ if self.req_model:
+ return self.req_model.from_args(*args, **kwds).model_dump(
+ exclude_none=True,
+ mode='json', # TODO: Maybe serialize Decimal in different way
+ )
+
+ raise NotImplementedError
+
+ def post_handle_payload(self, data: Any):
+ """Sorğunun payload-ının post-processing-i. Əgər sorğu göndərməmişdən qabaq
+ son datanın üzərinə əlavələr lazımdırsa, bu funksiyadan istifadə edə bilərsiniz.
+
+ Misal üçün: Bax [`EPointClientClass`][integrify.epoint.client.EPointClientClass]
+
+ Args:
+ data: `pre_handle_payload` və `handle_payload` funksiyalarından yaradılmış data.
+ """
+ return data # pragma: no cover
+
+ def handle_request(self, *args, **kwds):
+ """Sorğu üçün payload-u hazırlayan funksiya. Üç mərhələ icra edir,
+ və bu mərhələlər override oluna bilər. (Misal üçün:
+ Bax [`EPointClientClass`][integrify.epoint.client.EPointClientClass])
+
+ 1. Pre-processing
+ 2. Payload hazırlama
+ 3. Post-processing
+ """
+
+ pre_data = self.pre_handle_payload(*args, **kwds) or {}
+ data = {**pre_data, **self.handle_payload(*args, **kwds)}
+ return self.post_handle_payload(data)
+
+ def handle_response(
+ self,
+ resp: httpx.Response,
+ ) -> Union[APIResponse[ResponseType], httpx.Response]:
+ """Sorğudan gələn cavab payload-ı handle edən funksiya. `self.resp_model` schema-sı
+ verilibsə, onunla parse və validate olunur, əks halda, json/dict formatında qaytarılır.
+ """
+ if self.resp_model:
+ return APIResponse[self.resp_model].model_validate(resp, from_attributes=True) # type: ignore[name-defined]
+
+ return resp
+
+
+class APIExecutor:
+ """API sorgularını icra edən class"""
+
+ def __init__(self, name: str, sync: bool = True):
+ """
+ Args:
+ name: API klientin adı. Logging üçün istifadə olunur.
+ sync: Sync (True) və ya Async (False) klient seçimi. Default olaraq sync seçilir.
+ """
+ self.sync = sync
+ self.client_name = name
+ self.logger = LOGGER_FUNCTION(name)
+
+ self.client: Union[httpx.Client, httpx.AsyncClient]
+ """httpx sorğu client-i"""
+
+ if sync:
+ self.client = httpx.Client(timeout=10)
+ else:
+ self.client = httpx.AsyncClient(timeout=10)
+
+ @property
+ def request_function(
+ self,
+ ) -> Callable[
+ [str, str, Optional['APIPayloadHandler'], Any], # input args
+ Union[APIResponse[ResponseType], Coroutine[Any, Any, APIResponse[ResponseType]]], # output
+ ]:
+ """Sync/async request atan funksiyanı seçən attribute"""
+ if self.sync:
+ return lambda *args, **kwds: self.sync_req(*args, **kwds)
+ else:
+ return lambda *args, **kwds: self.async_req(*args, **kwds)
+
+ def sync_req(self, url: str, verb: str, handler: Optional['APIPayloadHandler'], *args, **kwds):
+ """Sync sorğu atan funksiya
+
+ Args:
+ url: Sorğunun full url-i
+ verb: Sorğunun metodun (`POST`, `GET`, və s.)
+ handler: Sorğu və cavabın payload handler-i
+ """
+ assert isinstance(self.client, httpx.Client)
+
+ data = handler.handle_request(*args, **kwds) if handler else None
+ headers = handler.headers if handler else None
+
+ response = self.client.request(verb, url, data=data, headers=headers)
+
+ if not response.is_success:
+ self.logger.error(
+ f'{self.client_name} request to {url} failed. '
+ f'Status code was {response.status_code}. '
+ f'Content => {response.content.decode()}'
+ )
+
+ if handler:
+ return handler.handle_response(response)
+
+ return response
+
+ async def async_req( # pragma: no cover
+ self, url: str, verb: str, handler: Optional['APIPayloadHandler'], *args, **kwds
+ ):
+ """Async sorğu atan funksiya
+
+ Args:
+ url: Sorğunun full url-i
+ verb: Sorğunun metodun (`POST`, `GET`, və s.)
+ handler: Sorğu və cavabın payload handler-i
+ """
+ assert isinstance(self.client, httpx.AsyncClient)
+
+ data = handler.handle_request(*args, **kwds) if handler else None
+ headers = handler.headers if handler else None
+
+ response = await self.client.request(verb, url, data=data, headers=headers)
+
+ if not response.is_success:
+ self.logger.error(
+ f'{self.client_name} request to {url} failed. '
+ f'Status code was {response.status_code}. '
+ f'Content => {response.content.decode()}'
+ )
+
+ if handler:
+ return handler.handle_response(response)
+
+ return response
diff --git a/src/integrify/base.py b/src/integrify/base.py
deleted file mode 100644
index eb7e748..0000000
--- a/src/integrify/base.py
+++ /dev/null
@@ -1,98 +0,0 @@
-from typing import Any, Generic, Optional, TypeVar, Union
-from urllib.parse import urljoin
-
-import httpx
-from pydantic import BaseModel, Field
-
-from integrify.logger import LOGGER_FUNCTION
-
-ResponseType = TypeVar('ResponseType', bound=BaseModel)
-
-
-class ApiResponse(BaseModel, Generic[ResponseType]):
- ok: bool = Field(validation_alias='is_success')
- """Cavab sorğusunun statusu 400dən kiçikdirsə"""
-
- status_code: int
- """Cavab sorğusunun status kodu"""
-
- headers: dict
- """Cavab sorğusunun header-i"""
-
- body: ResponseType
- """Cavab sorğusunun body-si"""
-
-
-def send_request(func):
- def wrapper(self, *args, **kwargs) -> ApiResponse[ResponseType]:
- func(self, *args, **kwargs)
- return self.__call__()
-
- return wrapper
-
-
-class ApiRequest:
- session: Union[httpx.Client, httpx.AsyncClient, None] = None
-
- def __init__(self, client_name: str, logger_name: str):
- self.base_url: str
- self.path: str
- self.verb: Optional[str] = None
- self.headers: dict = {}
- self.body: dict = {}
-
- #
- self.client_name: str = client_name
- self.logger = LOGGER_FUNCTION(logger_name)
-
- self.resp_model: type[ResponseType] # type: ignore[valid-type]
- self.accept_status_codes: list[int] = [200, 201, 204]
-
- @property
- def url(self):
- return urljoin(self.base_url, self.path)
-
- def process_response(self, resp: httpx.Response):
- resp.body = resp.json() # type: ignore[attr-defined]
-
- if not resp.is_success:
- self.logger.error(
- f'{self.client_name} failed. Status code was {resp.status_code}. '
- f'Content => {resp.body}' # type: ignore[attr-defined]
- )
- elif resp.status_code not in self.accept_status_codes:
- self.logger.error(
- f'{self.client_name} response is not in {self.accept_status_codes}. '
- f'Status: {resp.status_code}, body: {resp.body}' # type: ignore[attr-defined]
- )
-
- return ApiResponse[self.resp_model].model_validate(resp, from_attributes=True) # type: ignore[name-defined]
-
- def __call__(self, *args: Any, **kwds: Any): ...
-
-
-class SyncApiRequest(ApiRequest):
- session: Optional[httpx.Client] = None
-
- def __call__(self, *args: Any, **kwds: Any):
- if not self.session:
- self.session = httpx.Client(timeout=10)
-
- resp = self.session.request(self.verb, self.url, data=self.body, headers=self.headers) # type: ignore[arg-type]
- return self.process_response(resp)
-
-
-class AsyncApiRequest(ApiRequest):
- session: Optional[httpx.AsyncClient] = None
-
- async def __call__(self, *args, **kwds):
- if not self.session:
- self.session = httpx.AsyncClient(timeout=10)
-
- resp = await self.session.request(
- self.verb,
- self.url,
- data=self.body,
- headers=self.headers,
- )
- return self.process_response(resp)
diff --git a/src/integrify/epoint/__init__.py b/src/integrify/epoint/__init__.py
index 57d7a58..5d03b61 100644
--- a/src/integrify/epoint/__init__.py
+++ b/src/integrify/epoint/__init__.py
@@ -6,7 +6,7 @@
RU: https://epointbucket.s3.eu-central-1.amazonaws.com/files/instructions/API%20Epoint%20ru.pdf
"""
+from .client import EPointAsyncRequest, EPointClientClass, EPointRequest
from .env import VERSION
-from .sync import EPointRequest
-__all__ = ['EPointRequest', 'VERSION']
+__all__ = ['EPointAsyncRequest', 'EPointClientClass', 'EPointRequest', 'VERSION']
diff --git a/src/integrify/epoint/asyncio.py b/src/integrify/epoint/asyncio.py
deleted file mode 100644
index c053ab2..0000000
--- a/src/integrify/epoint/asyncio.py
+++ /dev/null
@@ -1,352 +0,0 @@
-import base64
-import json
-from decimal import Decimal
-from typing import Any, Coroutine, Optional
-
-from integrify.base import ApiResponse, AsyncApiRequest
-from integrify.epoint.helper import generate_signature
-from integrify.epoint.schemas.response import (
- BaseResponseSchema,
- MinimalResponseSchema,
- RedirectUrlResponseSchema,
- RedirectUrlWithCardIdResponseSchema,
- SplitPayWithSavedCardResponseSchema,
- TransactionStatusResponseSchema,
-)
-from integrify.epoint.sync import EPointRequestClass as SyncEPointRequest
-
-__all__ = ['EPointRequest']
-
-
-class EPointRequestClass(AsyncApiRequest, SyncEPointRequest):
- """EPoint async sorğular üçün baza class"""
-
- async def pay( # type: ignore[override]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- description: str | None = None,
- **extra: Any,
- ) -> Coroutine[Any, Any, ApiResponse[RedirectUrlResponseSchema]]:
- """Ödəniş sorğusu (async)
-
- **Endpoint:** */api/1/request*
-
- Example:
- ```python
- await EPointRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
- ```
-
- **Cavab formatı**: `RedirectUrlResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` gəlir. Müştəri həmin URLə daxil
- olub, kart məlumatlarını daxil edib, uğurlu ödəniş etdikdən sonra, backend callback
- APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur, və eyni `order_id`
- ilə EPointDecodedCallbackDataSchema formatında məlumat gəlir.
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- **extra: Başqa ötürmək istədiyiniz əlavə dəyərlər. Bu dəyərlər callback sorğuda sizə
- geri göndərilir.
- """ # noqa: E501
- return await super().pay(amount, currency, order_id, description, **extra) # type: ignore[misc]
-
- async def get_transaction_status(
- self,
- transaction_id: str,
- ) -> Coroutine[Any, Any, ApiResponse[TransactionStatusResponseSchema]]:
- """
- Transaksiya statusunu öyrənmək üçün sorğu (async)
-
- **Endpoint:** */api/1/get-status*
-
- Example:
- ```python
- await EPointRequest.get_transaction_status(transaction_id='texxxxxx')
- ```
-
- Cavab formatı: `TransactionStatusResponseSchema`
-
- Args:
- transaction_id: EPoint tərəfindən verilmiş tranzaksiya IDsi.
- Adətən `te` prefiksi ilə olur.
- """
- return await super().get_transaction_status(transaction_id)
-
- async def save_card(
- self,
- ) -> Coroutine[Any, Any, ApiResponse[RedirectUrlWithCardIdResponseSchema]]:
- """Ödəniş olmadan kartı yadda saxlamaq sorğusu (async)
-
- **Endpoint:** */api/1/card-registration*
-
- Example:
- ```python
- await EPointRequest.save_card()
- ```
-
- Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` və `card_id` gəlir.
- Müştəri həmin URLə daxil olub, kart məlumatlarını uğurlu qeyd etdikdən sonra,
- backend callback APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur,
- və eyni `card_id` ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
- """
- return await super().save_card()
-
- async def pay_with_saved_card(
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- card_id: str,
- ) -> Coroutine[Any, Any, ApiResponse[BaseResponseSchema]]:
- """Yadda saxlanılmış kartla ödəniş sorğusu (async)
-
- **Endpoint:** */api/1/execute-pay*
-
- Example:
- ```python
- await EPointRequest.pay_with_saved_card(amount=100, currency='AZN', order_id='12345678', card_id='cexxxxxx')
- ```
-
- Cavab formatı: `BaseResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `BaseResponseSchema` formatında
- cavab gəlir, və ödənişin statusu birbaşa qayıdır: heç bir callback sorğusu gəlmir.
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
- """ # noqa: E501
- return await super().pay_with_saved_card(amount, currency, order_id, card_id)
-
- async def pay_and_save_card(
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- description: Optional[str] = None,
- ) -> Coroutine[Any, Any, ApiResponse[RedirectUrlWithCardIdResponseSchema]]:
- """Ödəniş və kartı yadda saxlama sorğusu (async)
-
- **Endpoint:** */api/1/card-registration-with-pay*
-
- Example:
- ```python
- await EPointRequest.pay_and_save_card(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
- ```
-
- Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` və `card_id` gəlir. Müştəri həmin URLə
- daxil olub, kart məlumatlarını daxil edib, uğurlu ödəniş etdikdən sonra, backend callback
- APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur, və eyni `order_id` və
- `card_id` ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- return await super().pay_and_save_card(amount, currency, order_id, description)
-
- async def payout(
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- card_id: str,
- description: Optional[str] = None,
- ) -> Coroutine[Any, Any, ApiResponse[BaseResponseSchema]]:
- """Hesabınızda olan pulu karta nağdlaşdırmaq sorğusu (async)
-
- **Endpoint:** */api/1/refund-request*
-
- Example:
- ```python
- await EPointRequest.payout(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
- ```
-
- Cavab sorğu formatı: `BaseResponseSchema`
-
- Bu sorğunu göndərdikdə, əməliyyat Epoint xidməti tərəfindən işləndikdən və bankdan ödəniş
- statusu alındıqdan sonra cavab `BaseResponseSchema` formatında qayıdacaqdır
-
- Args:
- amount: Nağdlaşdırmaq miqdarı. Numerik dəyər.
- currency: Nağdlaşdırma məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
- description: Nağdlaşdırmanın təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- return await super().payout(amount, currency, order_id, card_id, description)
-
- async def refund(
- self,
- transaction_id: str,
- currency: str,
- amount: Optional[Decimal] = None,
- ) -> Coroutine[Any, Any, ApiResponse[MinimalResponseSchema]]:
- """Keçmiş ödənişi tam və ya yarımçıq geri qaytarma sorğusu (async)
-
- **Endpoint:** */api/1/reverse*
-
- Examples:
- ```python
- # Full refund
- await EPointRequest.refund(transaction_id='texxxxxx', currency='AZN')
-
- # Partial refund
- await EPointRequest.refund(transaction_id='texxxxxx', currency='AZN', amount=50)
- ```
-
- Cavab formatı: `MinimalResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `status` və `message` gəlir.
- Heç bir callback sorğusu göndərilmir.
-
- Args:
- transaction_id: EPoint tərəfindən verilmiş tranzaksiya IDsi.
- Adətən `te` prefiksi ilə olur.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- amount: Ödəniş məbləği. Məbləğin göndərilməsi yarımçıq geri-qaytarma hesab olunur,
- əks halda tam geri-qaytarma baş verəcəkdir.
- """
- return await super().refund(transaction_id, currency, amount)
-
- async def split_pay(
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- split_user_id: str,
- split_amount: Decimal,
- description: Optional[str] = None,
- **extra: Any,
- ) -> Coroutine[Any, Any, ApiResponse[RedirectUrlResponseSchema]]:
- """Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə sorğusu (async)
-
- **Endpoint:** */api/1/split-request*
-
- Example:
- ```python
- await EPointRequest.split_pay(amount=100, currency='AZN', order_id='123456789', split_user_id='epoint_user_id', split_amount=50, description='split payment')
- ```
-
- Cavab formatı: `RedirectUrlResponseSchema`
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
- split_amount: Bölünən miqdar. Numerik dəyər
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- **extra: Başqa ötürmək istədiyiniz əlavə dəyərlər. Bu dəyərlər callback sorğuda sizə
- geri göndərilir.
- """ # noqa: E501
- return await super().split_pay(
- amount,
- currency,
- order_id,
- split_user_id,
- split_amount,
- description,
- **extra,
- )
-
- async def split_pay_with_saved_card(
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- card_id: str,
- split_user_id: str,
- split_amount: Decimal,
- description: Optional[str] = None,
- ) -> Coroutine[Any, Any, ApiResponse[SplitPayWithSavedCardResponseSchema]]:
- """Saxlanılmış kartla ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə sorğusu (async)
-
- **Endpoint:** */api/1/split-execute-pay*
-
- Example:
- ```python
- await EPointRequest.split_pay_with_saved_card(amount=100, currency='AZN', order_id='123456789', card_id='cexxxxxx', split_user_id='epoint_user_id', split_amount=50, description='split payment')
- ```
-
- Cavab formatı: `SplitPayWithSavedCardResponseSchema`
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
- split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
- split_amount: Bölünən miqdar. Numerik dəyər
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- return super().split_pay_with_saved_card(
- amount,
- currency,
- order_id,
- card_id,
- split_user_id,
- split_amount,
- description,
- )
-
- async def split_pay_and_save_card(
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- split_user_id: str,
- split_amount: Decimal,
- description: Optional[str] = None,
- ) -> Coroutine[Any, Any, ApiResponse[RedirectUrlWithCardIdResponseSchema]]:
- """Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə və kartı saxlama sorğusu (sync)
-
- **Endpoint:** */api/1/split-card-registration-with-pay*
-
- Example:
- ```python
- await EPointRequest.split_pay_and_save_card(amount=100, currency='AZN', order_id='123456789', split_user_id='epoint_user_id', split_amount=50, description='split payment')
- ```
-
- Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
- split_amount: Bölünən miqdar. Numerik dəyər
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- return super().split_pay_and_save_card(
- amount,
- currency,
- order_id,
- split_user_id,
- split_amount,
- description,
- )
-
- async def __call__(self, *args, **kwargs):
- b64data = base64.b64encode(json.dumps(self.data).encode()).decode()
- self.body = {
- 'data': b64data,
- 'signature': generate_signature(b64data),
- }
- return await super().__call__(*args, **kwargs)
-
-
-EPointRequest = EPointRequestClass()
diff --git a/src/integrify/epoint/client.py b/src/integrify/epoint/client.py
new file mode 100644
index 0000000..dac195d
--- /dev/null
+++ b/src/integrify/epoint/client.py
@@ -0,0 +1,378 @@
+from typing import TYPE_CHECKING, Any, Optional
+from typing import SupportsFloat as Numeric
+
+from integrify.api import APIClient, APIResponse
+from integrify.epoint import env
+from integrify.epoint.handlers import (
+ GetTransactionStatusPayloadHandler,
+ PayAndSaveCardPayloadHandler,
+ PaymentPayloadHandler,
+ PayoutPayloadHandler,
+ PayWithSavedCardPayloadHandler,
+ RefundPayloadHandler,
+ SaveCardPayloadHandler,
+ SplitPayAndSaveCardPayloadHandler,
+ SplitPayPayloadHandler,
+ SplitPayWithSavedCardPayloadHandler,
+)
+from integrify.epoint.schemas.response import (
+ BaseResponseSchema,
+ MinimalResponseSchema,
+ RedirectUrlResponseSchema,
+ RedirectUrlWithCardIdResponseSchema,
+ SplitPayWithSavedCardResponseSchema,
+ TransactionStatusResponseSchema,
+)
+
+__all__ = ['EPointClientClass']
+
+
+class EPointClientClass(APIClient):
+ """EPoint sorğular üçün baza class"""
+
+ def __init__(self, sync: bool = True):
+ super().__init__('EPoint', 'https://epoint.az', None, sync)
+
+ self.add_url('pay', env.API.PAY)
+ self.add_handler('pay', PaymentPayloadHandler)
+
+ self.add_url('get_transaction_status', env.API.GET_STATUS)
+ self.add_handler('get_transaction_status', GetTransactionStatusPayloadHandler)
+
+ self.add_url('save_card', env.API.SAVE_CARD)
+ self.add_handler('save_card', SaveCardPayloadHandler)
+
+ self.add_url('pay_with_saved_card', env.API.PAY_WITH_SAVED_CARD)
+ self.add_handler('pay_with_saved_card', PayWithSavedCardPayloadHandler)
+
+ self.add_url('pay_and_save_card', env.API.PAY_AND_SAVE_CARD)
+ self.add_handler('pay_and_save_card', PayAndSaveCardPayloadHandler)
+
+ self.add_url('payout', env.API.PAYOUT)
+ self.add_handler('payout', PayoutPayloadHandler)
+
+ self.add_url('refund', env.API.REFUND)
+ self.add_handler('refund', RefundPayloadHandler)
+
+ self.add_url('split_pay', env.API.SPLIT_PAY)
+ self.add_handler('split_pay', SplitPayPayloadHandler)
+
+ self.add_url('split_pay_with_saved_card', env.API.SPLIT_PAY_WITH_SAVED_CARD)
+ self.add_handler('split_pay_with_saved_card', SplitPayWithSavedCardPayloadHandler)
+
+ self.add_url('split_pay_and_save_card', env.API.SPLIT_PAY_AND_SAVE_CARD)
+ self.add_handler('split_pay_and_save_card', SplitPayAndSaveCardPayloadHandler)
+
+ def add_url(self, route_name, url):
+ # All Epoint requests are POST
+ return super().add_url(route_name, url, 'POST')
+
+ if TYPE_CHECKING:
+
+ def pay(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ description: Optional[str] = None,
+ **extra: Any,
+ ) -> APIResponse[RedirectUrlResponseSchema]:
+ """Ödəniş sorğusu
+
+ **Endpoint:** */api/1/request*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
+ ```
+
+ **Cavab formatı**: `RedirectUrlResponseSchema`
+
+ Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` gəlir. Müştəri həmin URLə daxil
+ olub, kart məlumatlarını daxil edib, uğurlu ödəniş etdikdən sonra, backend callback
+ APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur, və eyni `order_id`
+ ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
+ **extra: Başqa ötürmək istədiyiniz əlavə dəyərlər. Bu dəyərlər callback sorğuda sizə
+ geri göndərilir.
+ """ # noqa: E501
+
+ def get_transaction_status(
+ self,
+ transaction_id: str,
+ ) -> APIResponse[TransactionStatusResponseSchema]:
+ """
+ Transaksiya statusunu öyrənmək üçün sorğu
+
+ **Endpoint:** */api/1/get-status*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.get_transaction_status(transaction_id='texxxxxx')
+ ```
+
+ Cavab formatı: `TransactionStatusResponseSchema`
+
+ Args:
+ transaction_id: EPoint tərəfindən verilmiş tranzaksiya IDsi.
+ Adətən `te` prefiksi ilə olur.
+ """
+
+ def save_card(self) -> APIResponse[RedirectUrlWithCardIdResponseSchema]:
+ """Ödəniş olmadan kartı yadda saxlamaq sorğusu
+
+ **Endpoint:** */api/1/card-registration*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.save_card()
+ ```
+
+ Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
+
+ Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` və `card_id` gəlir.
+ Müştəri həmin URLə daxil olub, kart məlumatlarını uğurlu qeyd etdikdən sonra,
+ backend callback APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur,
+ və eyni `card_id` ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
+ """
+
+ def pay_with_saved_card(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ card_id: str,
+ ) -> APIResponse[BaseResponseSchema]:
+ """Yadda saxlanılmış kartla ödəniş sorğusu
+
+ **Endpoint:** */api/1/execute-pay*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.pay_with_saved_card(amount=100, currency='AZN', order_id='12345678', card_id='cexxxxxx')
+ ```
+
+ Cavab formatı: `BaseResponseSchema`
+
+ Bu sorğunu göndərdikdə, cavab olaraq `BaseResponseSchema` formatında
+ cavab gəlir, və ödənişin statusu birbaşa qayıdır: heç bir callback sorğusu gəlmir.
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
+ """ # noqa: E501
+
+ def pay_and_save_card(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ description: Optional[str] = None,
+ ) -> APIResponse[RedirectUrlWithCardIdResponseSchema]:
+ """Ödəniş və kartı yadda saxlama sorğusu
+
+ **Endpoint:** */api/1/card-registration-with-pay*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.pay_and_save_card(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
+ ```
+
+ Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
+
+ Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` və `card_id` gəlir. Müştəri həmin URLə
+ daxil olub, kart məlumatlarını daxil edib, uğurlu ödəniş etdikdən sonra, backend callback
+ APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur, və eyni `order_id` və
+ `card_id` ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
+ """ # noqa: E501
+
+ def payout(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ card_id: str,
+ description: Optional[str] = None,
+ ) -> APIResponse[BaseResponseSchema]:
+ """Hesabınızda olan pulu karta nağdlaşdırmaq sorğusu
+
+ **Endpoint:** */api/1/refund-request*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.payout(amount=100, currency='AZN', order_id='12345678', card_id='cexxxxxx', description='Ödəniş')
+ ```
+
+ Cavab sorğu formatı: `BaseResponseSchema`
+
+ Bu sorğunu göndərdikdə, əməliyyat Epoint xidməti tərəfindən işləndikdən və bankdan ödəniş
+ statusu alındıqdan sonra cavab `BaseResponseSchema` formatında qayıdacaqdır
+
+ Args:
+ amount: Nağdlaşdırmaq miqdarı. Numerik dəyər.
+ currency: Nağdlaşdırma məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
+ description: Nağdlaşdırmanın təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
+ """ # noqa: E501
+
+ def refund(
+ self,
+ transaction_id: str,
+ currency: str,
+ amount: Optional[Numeric] = None,
+ ) -> APIResponse[MinimalResponseSchema]:
+ """Keçmiş ödənişi tam və ya yarımçıq geri qaytarma sorğusu
+
+ **Endpoint:** */api/1/reverse*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ # Full refund
+ EPointRequest.refund(transaction_id='texxxxxx', currency='AZN')
+
+ # Partial refund
+ EPointRequest.refund(transaction_id='texxxxxx', currency='AZN', amount=50)
+ ```
+
+ Cavab formatı: `MinimalResponseSchema`
+
+ Bu sorğunu göndərdikdə, cavab olaraq `status` və `message` gəlir.
+ Heç bir callback sorğusu göndərilmir.
+
+ Args:
+ transaction_id: EPoint tərəfindən verilmiş tranzaksiya IDsi.
+ Adətən `te` prefiksi ilə olur.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ amount: Ödəniş məbləği. Məbləğin göndərilməsi yarımçıq geri-qaytarma hesab olunur,
+ əks halda tam geri-qaytarma baş verəcəkdir.
+ """
+
+ def split_pay(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ split_user_id: str,
+ split_amount: Numeric,
+ description: Optional[str] = None,
+ **extra: Any,
+ ) -> APIResponse[RedirectUrlResponseSchema]:
+ """Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə sorğusu
+
+ **Endpoint:** */api/1/split-request*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.split_pay(amount=100, currency='AZN', order_id='123456789', split_user_id='epoint_user_id', split_amount=50, description='split payment')
+ ```
+
+ Cavab formatı: `RedirectUrlResponseSchema`
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
+ split_amount: Bölünən miqdar. Numerik dəyər
+ description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
+ **extra: Başqa ötürmək istədiyiniz əlavə dəyərlər. Bu dəyərlər callback sorğuda sizə
+ geri göndərilir.
+ """ # noqa: E501
+
+ def split_pay_with_saved_card(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ card_id: str,
+ split_user_id: str,
+ split_amount: Numeric,
+ description: Optional[str] = None,
+ ) -> APIResponse[SplitPayWithSavedCardResponseSchema]:
+ """Saxlanılmış kartla ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə sorğusu
+
+ **Endpoint:** */api/1/split-execute-pay*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.split_pay_with_saved_card(amount=100, currency='AZN', order_id='123456789', card_id='cexxxxxx', split_user_id='epoint_user_id', split_amount=50, description='split payment')
+ ```
+
+ Cavab formatı: `SplitPayWithSavedCardResponseSchema`
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
+ split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
+ split_amount: Bölünən miqdar. Numerik dəyər
+ description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
+ """ # noqa: E501
+
+ def split_pay_and_save_card(
+ self,
+ amount: Numeric,
+ currency: str,
+ order_id: str,
+ split_user_id: str,
+ split_amount: Numeric,
+ description: Optional[str] = None,
+ ) -> APIResponse[RedirectUrlWithCardIdResponseSchema]:
+ """Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə və kartı saxlama sorğusu
+
+ **Endpoint:** */api/1/split-card-registration-with-pay*
+
+ Example:
+ ```python
+ from integrify.epoint import EPointRequest
+
+ EPointRequest.split_pay_and_save_card(amount=100, currency='AZN', order_id='123456789', split_user_id='epoint_user_id', split_amount=50, description='split payment')
+ ```
+
+ Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
+
+ Args:
+ amount: Ödəniş miqdarı. Numerik dəyər.
+ currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
+ order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
+ split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
+ split_amount: Bölünən miqdar. Numerik dəyər
+ description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
+ """ # noqa: E501
+
+
+EPointRequest = EPointClientClass(sync=True)
+EPointAsyncRequest = EPointClientClass(sync=False)
diff --git a/src/integrify/epoint/env.py b/src/integrify/epoint/env.py
index 8a082d4..b982c18 100644
--- a/src/integrify/epoint/env.py
+++ b/src/integrify/epoint/env.py
@@ -16,7 +16,7 @@
if not EPOINT_PUBLIC_KEY or not EPOINT_PRIVATE_KEY:
warn(
- 'EPOINT_PUBLIC_KEY/EPOINT_PRIVATE_KEY mühit dəyişənlərinə dəyər verməsəniz'
+ 'EPOINT_PUBLIC_KEY/EPOINT_PRIVATE_KEY mühit dəyişənlərinə dəyər verməsəniz '
'sorğular çalışmayacaq!'
)
@@ -24,8 +24,8 @@
class API(str, Enum):
PAY: Literal['/api/1/request'] = '/api/1/request'
GET_STATUS: Literal['/api/1/get-status'] = '/api/1/get-status'
- CARD_REGISTRATION: Literal['/api/1/card-registration'] = '/api/1/card-registration'
- PAY_WITH_CARD: Literal['/api/1/execute-pay'] = '/api/1/execute-pay'
+ SAVE_CARD: Literal['/api/1/card-registration'] = '/api/1/card-registration'
+ PAY_WITH_SAVED_CARD: Literal['/api/1/execute-pay'] = '/api/1/execute-pay'
PAY_AND_SAVE_CARD: Literal['/api/1/card-registration-with-pay'] = (
'/api/1/card-registration-with-pay'
)
@@ -46,4 +46,5 @@ class API(str, Enum):
'EPOINT_SUCCESS_REDIRECT_URL',
'EPOINT_FAILED_REDIRECT_URL',
'EPOINT_LOGGER_NAME',
+ 'API',
]
diff --git a/src/integrify/epoint/handlers.py b/src/integrify/epoint/handlers.py
new file mode 100644
index 0000000..265fd20
--- /dev/null
+++ b/src/integrify/epoint/handlers.py
@@ -0,0 +1,116 @@
+import base64
+import json
+from typing import Type
+
+import httpx
+
+from integrify.api import APIPayloadHandler, ResponseType
+from integrify.epoint import env
+from integrify.epoint.helper import generate_signature
+from integrify.epoint.schemas.parts import TransactionStatus, TransactionStatusExtended
+from integrify.epoint.schemas.request import (
+ GetTransactionStatusRequestSchema,
+ PayAndSaveCardRequestSchema,
+ PaymentRequestSchema,
+ PayoutRequestSchema,
+ PayWithSavedCardRequestSchema,
+ RefundRequestSchema,
+ SaveCardRequestSchema,
+ SplitPayAndSaveCardRequestSchema,
+ SplitPayRequestSchema,
+ SplitPayWithSavedCardRequestSchema,
+)
+from integrify.epoint.schemas.response import (
+ BaseResponseSchema,
+ MinimalResponseSchema,
+ RedirectUrlResponseSchema,
+ RedirectUrlWithCardIdResponseSchema,
+ SplitPayWithSavedCardResponseSchema,
+ TransactionStatusResponseSchema,
+)
+from integrify.schemas import APIResponse, PayloadBaseModel
+
+
+class BasePayloadHandler(APIPayloadHandler):
+ def __init__(self, req_model: Type[PayloadBaseModel], resp_model: Type[ResponseType]):
+ super().__init__(req_model, resp_model)
+
+ def pre_handle_payload(self, *args, **kwds):
+ return {
+ 'public_key': env.EPOINT_PUBLIC_KEY,
+ 'language': env.EPOINT_INTERFACE_LANG,
+ }
+
+ def post_handle_payload(self, data: dict):
+ b64data = base64.b64encode(json.dumps(data).encode()).decode()
+ return {
+ 'data': b64data,
+ 'signature': generate_signature(b64data),
+ }
+
+ def handle_response(self, resp: httpx.Response) -> APIResponse[ResponseType]:
+ api_resp: APIResponse[MinimalResponseSchema] = super().handle_response(resp) # type: ignore[assignment]
+
+ # EPoint həmişə 200 qaytarır, error olsa belə
+ if isinstance(api_resp.body.status, TransactionStatusExtended):
+ api_resp.ok = api_resp.body.status != TransactionStatusExtended.SERVER_ERROR
+ else:
+ api_resp.ok = api_resp.body.status == TransactionStatus.SUCCESS
+
+ return api_resp # type: ignore[return-value]
+
+
+class PaymentPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(PaymentRequestSchema, RedirectUrlResponseSchema)
+
+
+class GetTransactionStatusPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(GetTransactionStatusRequestSchema, TransactionStatusResponseSchema)
+
+
+class SaveCardPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(SaveCardRequestSchema, RedirectUrlWithCardIdResponseSchema)
+
+
+class PayWithSavedCardPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(PayWithSavedCardRequestSchema, BaseResponseSchema)
+
+
+class PayAndSaveCardPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(PayAndSaveCardRequestSchema, RedirectUrlWithCardIdResponseSchema)
+
+
+class PayoutPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(PayoutRequestSchema, BaseResponseSchema)
+
+
+class RefundPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(RefundRequestSchema, MinimalResponseSchema)
+
+
+class SplitPayPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(SplitPayRequestSchema, RedirectUrlResponseSchema)
+
+
+class SplitPayWithSavedCardPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(
+ SplitPayWithSavedCardRequestSchema,
+ SplitPayWithSavedCardResponseSchema,
+ )
+
+
+class SplitPayAndSaveCardPayloadHandler(BasePayloadHandler):
+ def __init__(self):
+ super().__init__(
+ SplitPayAndSaveCardRequestSchema,
+ RedirectUrlWithCardIdResponseSchema,
+ )
diff --git a/src/integrify/epoint/helper.py b/src/integrify/epoint/helper.py
index 5cb627f..dafa4e3 100644
--- a/src/integrify/epoint/helper.py
+++ b/src/integrify/epoint/helper.py
@@ -13,7 +13,7 @@
if sys.version_info >= (3, 9):
_sha1 = partial(sha1, usedforsecurity=False)
else:
- _sha1 = sha1
+ _sha1 = sha1 # pragma: no cover
def generate_signature(data: str) -> str:
diff --git a/src/integrify/epoint/schemas/request.py b/src/integrify/epoint/schemas/request.py
new file mode 100644
index 0000000..0f98db4
--- /dev/null
+++ b/src/integrify/epoint/schemas/request.py
@@ -0,0 +1,70 @@
+from decimal import Decimal
+from typing import Optional
+
+from pydantic import Field
+
+from integrify.epoint import env
+from integrify.schemas import PayloadBaseModel
+
+
+class MinimalPaymentRequestSchema(PayloadBaseModel):
+ amount: Decimal
+ currency: str
+ order_id: str
+
+
+class BasePaymentRequestSchema(MinimalPaymentRequestSchema):
+ success_redirect_url: Optional[str] = env.EPOINT_SUCCESS_REDIRECT_URL
+ error_redirect_url: Optional[str] = env.EPOINT_FAILED_REDIRECT_URL
+ description: Optional[str] = None
+
+
+##############################################################################
+class PaymentRequestSchema(BasePaymentRequestSchema):
+ other_attr: Optional[dict] = None
+
+
+class GetTransactionStatusRequestSchema(PayloadBaseModel):
+ transaction: str = Field(validation_alias='transaction_id')
+
+
+class SaveCardRequestSchema(PayloadBaseModel):
+ pass
+
+
+class PayWithSavedCardRequestSchema(MinimalPaymentRequestSchema):
+ card_id: str
+
+
+class PayAndSaveCardRequestSchema(BasePaymentRequestSchema):
+ pass
+
+
+class PayoutRequestSchema(MinimalPaymentRequestSchema):
+ card_id: str
+ description: Optional[str] = None
+
+
+class RefundRequestSchema(PayloadBaseModel):
+ transaction: str = Field(validation_alias='transaction_id')
+ currency: str
+ amount: Optional[Decimal] = None
+
+
+class SplitPayRequestSchema(BasePaymentRequestSchema):
+ split_user: str = Field(validation_alias='split_user_id')
+ split_amount: Decimal
+ other_attr: Optional[dict] = None
+
+
+class SplitPayWithSavedCardRequestSchema(MinimalPaymentRequestSchema):
+ card_id: str
+ split_user: str = Field(validation_alias='split_user_id')
+ split_amount: Decimal
+ description: Optional[str] = None
+
+
+class SplitPayAndSaveCardRequestSchema(BasePaymentRequestSchema):
+ split_user: str = Field(validation_alias='split_user_id')
+ split_amount: Decimal
+ description: Optional[str] = None
diff --git a/src/integrify/epoint/sync.py b/src/integrify/epoint/sync.py
deleted file mode 100644
index 4e2ecd0..0000000
--- a/src/integrify/epoint/sync.py
+++ /dev/null
@@ -1,513 +0,0 @@
-import base64
-import json
-from decimal import Decimal
-from typing import Any, Optional
-
-from integrify.base import ApiResponse, SyncApiRequest, send_request
-from integrify.epoint import env
-from integrify.epoint.helper import generate_signature
-from integrify.epoint.schemas.response import (
- BaseResponseSchema,
- MinimalResponseSchema,
- RedirectUrlResponseSchema,
- RedirectUrlWithCardIdResponseSchema,
- SplitPayWithSavedCardResponseSchema,
- TransactionStatusResponseSchema,
-)
-
-__all__ = ['EPointRequest']
-
-
-class EPointRequestClass(SyncApiRequest):
- """EPoint sorğular üçün baza class"""
-
- def __init__(self):
- super().__init__('EPoint', env.EPOINT_LOGGER_NAME)
- self.base_url = 'https://epoint.az'
- self.data = {
- 'public_key': env.EPOINT_PUBLIC_KEY,
- 'language': env.EPOINT_INTERFACE_LANG,
- }
-
- @send_request
- def pay( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- description: Optional[str] = None,
- **extra: Any,
- ) -> ApiResponse[RedirectUrlResponseSchema]:
- """Ödəniş sorğusu (sync)
-
- **Endpoint:** */api/1/request*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.pay(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
- ```
-
- **Cavab formatı**: `RedirectUrlResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` gəlir. Müştəri həmin URLə daxil
- olub, kart məlumatlarını daxil edib, uğurlu ödəniş etdikdən sonra, backend callback
- APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur, və eyni `order_id`
- ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- **extra: Başqa ötürmək istədiyiniz əlavə dəyərlər. Bu dəyərlər callback sorğuda sizə
- geri göndərilir.
- """ # noqa: E501
- self.path = env.API.PAY
- self.verb = 'POST'
- self.resp_model = RedirectUrlResponseSchema
-
- # Required data
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- }
- )
-
- # Optional
- if description:
- self.data['description'] = description
-
- if env.EPOINT_SUCCESS_REDIRECT_URL:
- self.data['success_redirect_url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- if env.EPOINT_FAILED_REDIRECT_URL:
- self.data['error_redirect__url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- if extra:
- self.data['other_attr'] = extra
-
- @send_request
- def get_transaction_status( # type: ignore[return]
- self,
- transaction_id: str,
- ) -> ApiResponse[TransactionStatusResponseSchema]:
- """
- Transaksiya statusunu öyrənmək üçün sorğu (sync)
-
- **Endpoint:** */api/1/get-status*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.get_transaction_status(transaction_id='texxxxxx')
- ```
-
- Cavab formatı: `TransactionStatusResponseSchema`
-
- Args:
- transaction_id: EPoint tərəfindən verilmiş tranzaksiya IDsi.
- Adətən `te` prefiksi ilə olur.
- """
- self.verb = 'POST'
- self.path = env.API.GET_STATUS
- self.data['transaction'] = transaction_id
- self.resp_model = TransactionStatusResponseSchema
-
- @send_request
- def save_card(self) -> ApiResponse[RedirectUrlWithCardIdResponseSchema]: # type: ignore[return]
- """Ödəniş olmadan kartı yadda saxlamaq sorğusu (sync)
-
- **Endpoint:** */api/1/card-registration*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.save_card()
- ```
-
- Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` və `card_id` gəlir.
- Müştəri həmin URLə daxil olub, kart məlumatlarını uğurlu qeyd etdikdən sonra,
- backend callback APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur,
- və eyni `card_id` ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
- """
- self.path = env.API.CARD_REGISTRATION
- self.verb = 'POST'
- self.resp_model = RedirectUrlWithCardIdResponseSchema
-
- @send_request
- def pay_with_saved_card( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- card_id: str,
- ) -> ApiResponse[BaseResponseSchema]:
- """Yadda saxlanılmış kartla ödəniş sorğusu (sync)
-
- **Endpoint:** */api/1/execute-pay*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.pay_with_saved_card(amount=100, currency='AZN', order_id='12345678', card_id='cexxxxxx')
- ```
-
- Cavab formatı: `BaseResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `BaseResponseSchema` formatında
- cavab gəlir, və ödənişin statusu birbaşa qayıdır: heç bir callback sorğusu gəlmir.
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
- """ # noqa: E501
- self.path = env.API.PAY_WITH_CARD
- self.verb = 'POST'
- self.resp_model = BaseResponseSchema
-
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- 'card_id': card_id,
- }
- )
-
- @send_request
- def pay_and_save_card( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- description: Optional[str] = None,
- ) -> ApiResponse[RedirectUrlWithCardIdResponseSchema]:
- """Ödəniş və kartı yadda saxlama sorğusu (sync)
-
- **Endpoint:** */api/1/card-registration-with-pay*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.pay_and_save_card(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
- ```
-
- Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `redirect_url` və `card_id` gəlir. Müştəri həmin URLə
- daxil olub, kart məlumatlarını daxil edib, uğurlu ödəniş etdikdən sonra, backend callback
- APIsinə (EPoint dashboard-ında qeyd etdiyiniz) sorğu daxil olur, və eyni `order_id` və
- `card_id` ilə `DecodedCallbackDataSchema` formatında məlumat gəlir.
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- self.path = env.API.PAY_AND_SAVE_CARD
- self.verb = 'POST'
- self.resp_model = RedirectUrlWithCardIdResponseSchema
-
- # Required data
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- }
- )
-
- # Optional
- if description:
- self.data['description'] = description
-
- if env.EPOINT_SUCCESS_REDIRECT_URL:
- self.data['success_redirect_url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- if env.EPOINT_FAILED_REDIRECT_URL:
- self.data['error_redirect__url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- @send_request
- def payout( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- card_id: str,
- description: Optional[str] = None,
- ) -> ApiResponse[BaseResponseSchema]:
- """Hesabınızda olan pulu karta nağdlaşdırmaq sorğusu (sync)
-
- **Endpoint:** */api/1/refund-request*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.payout(amount=100, currency='AZN', order_id='12345678', description='Ödəniş')
- ```
-
- Cavab sorğu formatı: `BaseResponseSchema`
-
- Bu sorğunu göndərdikdə, əməliyyat Epoint xidməti tərəfindən işləndikdən və bankdan ödəniş
- statusu alındıqdan sonra cavab `BaseResponseSchema` formatında qayıdacaqdır
-
- Args:
- amount: Nağdlaşdırmaq miqdarı. Numerik dəyər.
- currency: Nağdlaşdırma məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
- description: Nağdlaşdırmanın təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- self.path = env.API.PAYOUT
- self.verb = 'POST'
- self.resp_model = BaseResponseSchema
-
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- 'card_id': card_id,
- }
- )
-
- if description:
- self.data['description'] = description
-
- @send_request
- def refund( # type: ignore[return]
- self,
- transaction_id: str,
- currency: str,
- amount: Optional[Decimal] = None,
- ) -> ApiResponse[MinimalResponseSchema]:
- """Keçmiş ödənişi tam və ya yarımçıq geri qaytarma sorğusu (sync)
-
- **Endpoint:** */api/1/reverse*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- # Full refund
- EPointRequest.refund(transaction_id='texxxxxx', currency='AZN')
-
- # Partial refund
- EPointRequest.refund(transaction_id='texxxxxx', currency='AZN', amount=50)
- ```
-
- Cavab formatı: `MinimalResponseSchema`
-
- Bu sorğunu göndərdikdə, cavab olaraq `status` və `message` gəlir.
- Heç bir callback sorğusu göndərilmir.
-
- Args:
- transaction_id: EPoint tərəfindən verilmiş tranzaksiya IDsi.
- Adətən `te` prefiksi ilə olur.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- amount: Ödəniş məbləği. Məbləğin göndərilməsi yarımçıq geri-qaytarma hesab olunur,
- əks halda tam geri-qaytarma baş verəcəkdir.
- """
- self.path = env.API.REFUND
- self.verb = 'POST'
- self.resp_model = MinimalResponseSchema
-
- self.data.update({'transaction': transaction_id, 'currency': currency})
-
- if amount:
- self.data['amount'] = str(amount)
-
- @send_request
- def split_pay( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- split_user_id: str,
- split_amount: Decimal,
- description: Optional[str] = None,
- **extra: Any,
- ) -> ApiResponse[RedirectUrlResponseSchema]:
- """Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə sorğusu (sync)
-
- **Endpoint:** */api/1/split-request*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.split_pay(amount=100, currency='AZN', order_id='123456789', split_user_id='epoint_user_id', split_amount=50, description='split payment')
- ```
-
- Cavab formatı: `RedirectUrlResponseSchema`
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
- split_amount: Bölünən miqdar. Numerik dəyər
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- **extra: Başqa ötürmək istədiyiniz əlavə dəyərlər. Bu dəyərlər callback sorğuda sizə
- geri göndərilir.
- """ # noqa: E501
- self.path = env.API.SPLIT_PAY
- self.verb = 'POST'
- self.resp_model = RedirectUrlResponseSchema
-
- # Required data
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- 'split_user': split_user_id,
- 'split_amount': split_amount,
- }
- )
-
- # Optional
- if description:
- self.data['description'] = description
-
- if env.EPOINT_SUCCESS_REDIRECT_URL:
- self.data['success_redirect_url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- if env.EPOINT_FAILED_REDIRECT_URL:
- self.data['error_redirect__url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- if extra:
- self.data['other_attr'] = extra
-
- @send_request
- def split_pay_with_saved_card( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- card_id: str,
- split_user_id: str,
- split_amount: Decimal,
- description: Optional[str] = None,
- ) -> ApiResponse[SplitPayWithSavedCardResponseSchema]:
- """Saxlanılmış kartla ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə sorğusu (sync)
-
- **Endpoint:** */api/1/split-execute-pay*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.split_pay_with_saved_card(amount=100, currency='AZN', order_id='123456789', card_id='cexxxxxx', split_user_id='epoint_user_id', split_amount=50, description='split payment')
- ```
-
- Cavab formatı: `SplitPayWithSavedCardResponseSchema`
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- card_id: Saxlanılmış kartın id-si. Adətən `ce` prefiksi ilə başlayır.
- split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
- split_amount: Bölünən miqdar. Numerik dəyər
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- self.path = env.API.SPLIT_PAY_WITH_SAVED_CARD
- self.verb = 'POST'
- self.resp_model = SplitPayWithSavedCardResponseSchema
-
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- 'card_id': card_id,
- 'split_user': split_user_id,
- 'split_amount': split_amount,
- }
- )
-
- # Optional
- if description:
- self.data['description'] = description
-
- @send_request
- def split_pay_and_save_card( # type: ignore[return]
- self,
- amount: Decimal,
- currency: str,
- order_id: str,
- split_user_id: str,
- split_amount: Decimal,
- description: Optional[str] = None,
- ) -> ApiResponse[RedirectUrlWithCardIdResponseSchema]:
- """Ödənişi başqa EPoint istifadəçisi ilə bölüb ödəmə və kartı saxlama sorğusu (sync)
-
- **Endpoint:** */api/1/split-card-registration-with-pay*
-
- Example:
- ```python
- from integrify.epoint import EPointRequest
-
- EPointRequest.split_pay_and_save_card(amount=100, currency='AZN', order_id='123456789', split_user_id='epoint_user_id', split_amount=50, description='split payment')
- ```
-
- Cavab formatı: `RedirectUrlWithCardIdResponseSchema`
-
- Args:
- amount: Ödəniş miqdarı. Numerik dəyər.
- currency: Ödəniş məzənnəsi. Mümkün dəyərlər: AZN
- order_id: Unikal ID. Maksimal uzunluq: 255 simvol.
- split_user_id: Ödənişi böləcəyini **EPoint** user-ini IDsi
- split_amount: Bölünən miqdar. Numerik dəyər
- description: Ödənişin təsviri. Maksimal uzunluq: 1000 simvol. Məcburi arqument deyil.
- """ # noqa: E501
- self.path = env.API.SPLIT_PAY_AND_SAVE_CARD
- self.verb = 'POST'
- self.resp_model = RedirectUrlWithCardIdResponseSchema
-
- # Required data
- self.data.update(
- {
- 'amount': amount,
- 'currency': currency,
- 'order_id': order_id,
- 'split_user': split_user_id,
- 'split_amount': split_amount,
- }
- )
-
- # Optional
- if description:
- self.data['description'] = description
-
- if env.EPOINT_SUCCESS_REDIRECT_URL:
- self.data['success_redirect_url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- if env.EPOINT_FAILED_REDIRECT_URL:
- self.data['error_redirect__url'] = env.EPOINT_SUCCESS_REDIRECT_URL
-
- def __call__(self, *args, **kwargs):
- b64data = base64.b64encode(json.dumps(self.data).encode()).decode()
- self.body = {
- 'data': b64data,
- 'signature': generate_signature(b64data),
- }
- return super().__call__(*args, **kwargs)
-
-
-EPointRequest = EPointRequestClass()
diff --git a/src/integrify/logger.py b/src/integrify/logger.py
index ffe8407..d00e37b 100644
--- a/src/integrify/logger.py
+++ b/src/integrify/logger.py
@@ -3,12 +3,12 @@
from typing import Callable
try:
- import logfire # type: ignore
+ import logfire # type: ignore[import-not-found]
except ModuleNotFoundError:
logfire = None
try:
- import loguru # type: ignore
+ import loguru # type: ignore[import-not-found]
except ModuleNotFoundError:
loguru = None
diff --git a/src/integrify/schemas.py b/src/integrify/schemas.py
new file mode 100644
index 0000000..8f8c37b
--- /dev/null
+++ b/src/integrify/schemas.py
@@ -0,0 +1,41 @@
+from typing import Generic, TypeVar, Union
+
+from pydantic import BaseModel, Field, field_validator
+
+ResponseType = TypeVar('ResponseType', bound=BaseModel)
+
+
+class APIResponse(BaseModel, Generic[ResponseType]):
+ """Cavab sorğu base payload tipi. Generic tip-i qeyd etmıəklə
+ sorğu cavabını validate edə bilərsiniz.
+ """
+
+ ok: bool = Field(validation_alias='is_success')
+ """Cavab sorğusunun statusu 400dən kiçikdirsə"""
+
+ status_code: int
+ """Cavab sorğusunun status kodu"""
+
+ headers: dict
+ """Cavab sorğusunun header-i"""
+
+ body: ResponseType = Field(validation_alias='content')
+ """Cavab sorğusunun body-si"""
+
+ @field_validator('body', mode='before')
+ def convert_to_dict(cls, v: Union[str, bytes]) -> dict:
+ """Binary content-i dict-ə çevirərək, validation-a hazır vəziyyətə gətirir."""
+ import json
+
+ return json.loads(v)
+
+
+class PayloadBaseModel(BaseModel):
+ @classmethod
+ def from_args(cls, *args, **kwds):
+ """Verilən `*args` və `**kwds` (və ya `**kwargs`) parametrlərini birləşdirərək
+ Pydantic validasiyası edən funksiya. Positional arqumentlər üçün (`*args`) Pydantic
+ modelindəki field-lərin ardıcıllığı və çağırılan funksiyada parametrlərinin ardıcıllığı
+ EYNİ OLMALIDIR, əks halda, bu method yararsızdır.
+ """
+ return cls.model_validate({**{k: v for k, v in zip(cls.model_fields.keys(), args)}, **kwds})
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..cc3fd23
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,9 @@
+import pytest
+from integrify.api import APIClient
+
+from tests.mocks import * # noqa: F403
+
+
+@pytest.fixture(scope='package')
+def api_client():
+ yield APIClient(None, 'base_url')
diff --git a/tests/epoint/conftest.py b/tests/epoint/conftest.py
index 9653a4a..2f901a1 100644
--- a/tests/epoint/conftest.py
+++ b/tests/epoint/conftest.py
@@ -1,32 +1,11 @@
-import base64
-import json
-from hashlib import sha1
-from typing import Any
-
import pytest
-from integrify.base import ApiResponse
-from integrify.epoint.schemas.parts import TransactionStatus
-from integrify.epoint.sync import EPointRequestClass
+from integrify.epoint.client import EPointClientClass
from pytest_mock import MockerFixture
from tests import epoint
from tests.epoint.mocks import * # noqa: F403
-class TestEPointRequest(EPointRequestClass):
- __test__ = False
-
- def __init__(self, resp_data: dict):
- super().__init__()
- self.resp_data = resp_data
-
-
-@pytest.fixture(autouse=True, scope='package')
-def epoint_request_mocker(package_mocker: MockerFixture):
- package_mocker.patch('integrify.base.SyncApiRequest.__call__', new=req)
- yield
-
-
@pytest.fixture(autouse=True, scope='package')
def epoint_setenv(package_mocker: MockerFixture):
package_mocker.patch('integrify.epoint.env.EPOINT_PUBLIC_KEY', epoint.EPOINT_PUBLIC_KEY)
@@ -41,39 +20,6 @@ def epoint_set_wrong_env(mocker: MockerFixture):
yield
-def epoint_mock_generate_signature(data: str):
- sgn_string = epoint.EPOINT_PRIVATE_KEY + data + epoint.EPOINT_PRIVATE_KEY
- return base64.b64encode(sha1(sgn_string.encode()).digest()).decode()
-
-
-def is_signature_ok(data: dict):
- if data['signature'] != epoint_mock_generate_signature(data['data']):
- return False
-
- return (
- json.loads(base64.b64decode(data['data']).decode())['public_key']
- == epoint.EPOINT_PUBLIC_KEY
- )
-
-
-def req(self: TestEPointRequest, *args, **kwds):
- resp: dict[str, Any]
-
- if not is_signature_ok(self.body):
- resp = {
- 'body': {
- 'status': TransactionStatus.SERVER_ERROR,
- 'message': 'Signature did not match',
- }
- }
- else:
- resp = self.resp_data.copy()
-
- if not resp:
- raise Exception('Mock response data should be provided')
-
- resp.setdefault('is_success', True)
- resp.setdefault('status_code', 200)
- resp.setdefault('headers', {})
-
- return ApiResponse[self.resp_model].model_validate(resp) # type: ignore[name-defined]
+@pytest.fixture(scope='package')
+def epoint_client():
+ yield EPointClientClass()
diff --git a/tests/epoint/mocks.py b/tests/epoint/mocks.py
index 07cc66a..6ce3ea0 100644
--- a/tests/epoint/mocks.py
+++ b/tests/epoint/mocks.py
@@ -1,17 +1,18 @@
-from decimal import Decimal
-
import pytest
-
+from httpx import Response
from integrify.epoint.schemas.parts import TransactionStatus, TransactionStatusExtended
+from pytest_mock import MockerFixture
MESSAGE_SUCCESS = 'Təsdiq edildi'
MESSAGE_SERVER_ERROR = 'Signature did not match'
+MESSAGE_TRANSACTION_FAIL = 'Kartda kifayət qədər balans yoxdur'
@pytest.fixture(scope='package')
-def epoint_mock_get_transaction_status_response():
- return {
- 'body': {
+def epoint_mock_get_transaction_status_success_response(package_mocker: MockerFixture):
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatusExtended.SUCCESS,
'message': MESSAGE_SUCCESS,
'transaction': 'texxxxxxxxxx',
@@ -21,52 +22,102 @@ def epoint_mock_get_transaction_status_response():
'rrn': 'RRN-123456789',
'card_mask': '*******1234',
'card_name': 'Name Surname',
- 'amount': Decimal(1),
+ 'amount': 1,
+ 'code': '',
+ 'order_id': 'random_order_id',
+ 'other_attr': None,
+ },
+ )
+
+
+@pytest.fixture(scope='package')
+def epoint_mock_get_transaction_status_failed_response():
+ # Request is successful, transaction was not
+ return Response(
+ status_code=200,
+ json={
+ 'status': TransactionStatusExtended.ERROR,
+ 'message': MESSAGE_TRANSACTION_FAIL,
+ 'transaction': 'texxxxxxxxxx',
+ 'bank_transaction': 'base64data',
+ 'bank_response': '',
+ 'operation_code': None,
+ 'rrn': 'RRN-123456789',
+ 'card_mask': '*******1234',
+ 'card_name': 'Name Surname',
+ 'amount': 1,
'code': '',
'order_id': 'random_order_id',
'other_attr': None,
- }
- }
+ },
+ )
+
+
+@pytest.fixture(scope='package')
+def epoint_mock_bad_signature_response():
+ return Response(
+ status_code=200,
+ json={
+ 'status': TransactionStatusExtended.SERVER_ERROR,
+ 'message': MESSAGE_SERVER_ERROR,
+ },
+ )
@pytest.fixture(scope='package')
def epoint_mock_save_card_response():
- return {
- 'body': {
- 'status': 'success',
+ return Response(
+ status_code=200,
+ json={
+ 'status': TransactionStatus.SUCCESS,
'redirect_url': 'https://epoint.az',
'card_id': 'cexxxxxxxxxx',
- }
- }
+ },
+ )
+
+
+@pytest.fixture(scope='package')
+def epoint_mock_save_card_failed_response():
+ return Response(
+ status_code=200,
+ json={
+ 'status': TransactionStatus.ERROR,
+ 'redirect_url': None,
+ 'card_id': None,
+ },
+ )
@pytest.fixture(scope='package')
def epoint_mock_payment_response():
- return {
- 'body': {
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatus.SUCCESS,
'transaction': 'texxxxxxxxxx',
'redirect_url': 'https://epoint.az/',
- }
- }
+ },
+ )
@pytest.fixture(scope='package')
def epoint_mock_pay_and_save_card_response():
- return {
- 'body': {
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatus.SUCCESS,
'transaction': 'texxxxxxxxxx',
'redirect_url': 'https://epoint.az/',
'card_id': 'cexxxxxxxxxx',
- }
- }
+ },
+ )
@pytest.fixture(scope='package')
def epoint_mock_pay_with_saved_card_response():
- return {
- 'body': {
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatus.SUCCESS,
'message': 'Approved',
'transaction': 'texxxxxxxxxx',
@@ -77,25 +128,53 @@ def epoint_mock_pay_with_saved_card_response():
'card_mask': '*******1234',
'card_name': 'Name Surname',
'amount': 1,
- }
- }
+ },
+ )
+
+
+@pytest.fixture(scope='package')
+def epoint_mock_payout_response():
+ return Response(
+ status_code=200,
+ json={
+ 'status': TransactionStatus.SUCCESS,
+ 'message': 'Approved',
+ 'transaction': 'texxxxxxxxxx',
+ 'bank_transaction': 'base64data',
+ 'bank_response': '',
+ 'rrn': 'RRN-123456789',
+ 'card_mask': '*******1234',
+ 'card_name': 'Name Surname',
+ 'amount': 1,
+ },
+ )
+
+
+@pytest.fixture(scope='package')
+def epoint_mock_refund_response():
+ return Response(
+ status_code=200,
+ json={'status': TransactionStatus.SUCCESS, 'message': 'Approved'},
+ )
@pytest.fixture(scope='package')
def epoint_mock_split_payment_response():
- return {
- 'body': {
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatus.SUCCESS,
'transaction': 'texxxxxxxxxx',
'redirect_url': 'https://epoint.az/',
- }
- }
+ },
+ )
@pytest.fixture(scope='package')
def epoint_mock_split_pay_with_saved_card_response():
- return {
- 'body': {
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatus.SUCCESS,
'message': 'Approved',
'transaction': 'texxxxxxxxxx',
@@ -107,17 +186,18 @@ def epoint_mock_split_pay_with_saved_card_response():
'card_name': 'Name Surname',
'amount': 100,
'split_amount': 50,
- }
- }
+ },
+ )
@pytest.fixture(scope='package')
def epoint_mock_split_pay_and_save_card_response():
- return {
- 'body': {
+ return Response(
+ status_code=200,
+ json={
'status': TransactionStatus.SUCCESS,
'transaction': 'texxxxxxxxxx',
'redirect_url': 'https://epoint.az/',
'card_id': 'cexxxxxxxxxx',
- }
- }
+ },
+ )
diff --git a/tests/epoint/test_helpers.py b/tests/epoint/test_helpers.py
new file mode 100644
index 0000000..d5deeed
--- /dev/null
+++ b/tests/epoint/test_helpers.py
@@ -0,0 +1,26 @@
+from integrify.epoint.helper import decode_callback_data
+from integrify.epoint.schemas.response import CallbackDataSchema
+
+
+def test_str_to_dict():
+ schema = CallbackDataSchema.model_validate(b'data=data&signature=signature')
+
+ assert schema.data == 'data'
+ assert schema.signature == 'signature'
+
+
+def test_wrong_signature_response():
+ schema = CallbackDataSchema.model_validate(b'data=data&signature=signature')
+
+ assert decode_callback_data(schema) is None
+
+
+def test_ok_signature_response():
+ # data is: {"status": "sucess"}
+ schema = CallbackDataSchema.model_validate(
+ b'data=eyJzdGF0dXMiOiAic3VjY2VzcyJ9&signature=MbNxhkmAsyWi%2B3dPJxHf0RPC5Fw%3D'
+ )
+
+ data = decode_callback_data(schema)
+
+ assert data.status == 'success'
diff --git a/tests/epoint/test_misc.py b/tests/epoint/test_misc.py
index ae307c4..172c0ad 100644
--- a/tests/epoint/test_misc.py
+++ b/tests/epoint/test_misc.py
@@ -1,27 +1,86 @@
+from httpx import Response
+from integrify.epoint.client import EPointClientClass
from integrify.epoint.schemas.parts import TransactionStatus, TransactionStatusExtended
-from tests.epoint.conftest import TestEPointRequest
+from pytest_mock import MockerFixture
+from tests.epoint.mocks import MESSAGE_TRANSACTION_FAIL
-def test_ok_signature(epoint_mock_get_transaction_status_response):
- resp = TestEPointRequest(epoint_mock_get_transaction_status_response).get_transaction_status(
- transaction_id='te002458186'
- )
- assert resp.body.status == TransactionStatusExtended.SUCCESS
+def test_ok_signature(
+ epoint_client: EPointClientClass,
+ epoint_mock_get_transaction_status_success_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_get_transaction_status_success_response,
+ ):
+ resp = epoint_client.get_transaction_status(transaction_id='texxxxxx')
+ assert resp.ok
+ assert resp.body.status == TransactionStatusExtended.SUCCESS
-def test_wrong_signature(epoint_set_wrong_env, epoint_mock_get_transaction_status_response):
- resp = TestEPointRequest(epoint_mock_get_transaction_status_response).get_transaction_status(
- transaction_id='te002458186'
- )
- assert resp.ok
- assert resp.body.status == TransactionStatusExtended.SERVER_ERROR
- assert resp.body.message == 'Signature did not match'
+def test_wrong_signature(
+ epoint_set_wrong_env,
+ epoint_client: EPointClientClass,
+ epoint_mock_bad_signature_response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_bad_signature_response,
+ ):
+ resp = epoint_client.get_transaction_status(transaction_id='texxxxxx')
+ assert not resp.ok
+ assert resp.body.status == TransactionStatusExtended.SERVER_ERROR
+ assert resp.body.message == 'Signature did not match'
-def test_epoint_save_card_request(epoint_mock_save_card_response):
- resp = TestEPointRequest(epoint_mock_save_card_response).save_card()
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.card_id
+def test_get_failed_transaction_status(
+ epoint_client: EPointClientClass,
+ epoint_mock_get_transaction_status_failed_response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_get_transaction_status_failed_response,
+ ):
+ resp = epoint_client.get_transaction_status(transaction_id='texxxxxx')
+
+ assert resp.ok
+ assert resp.body.status == TransactionStatusExtended.ERROR
+ assert resp.body.message == MESSAGE_TRANSACTION_FAIL
+
+
+def test_epoint_save_card_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_save_card_response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_save_card_response,
+ ):
+ resp = epoint_client.save_card()
+
+ assert resp.ok
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.card_id
+
+
+def test_epoint_save_card_failed_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_save_card_failed_response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_save_card_failed_response,
+ ):
+ resp = epoint_client.save_card()
+
+ assert not resp.ok
+ assert resp.body.status == TransactionStatus.ERROR
+ assert resp.body.card_id is None
diff --git a/tests/epoint/test_payment.py b/tests/epoint/test_payment.py
index 51aad82..1507cfe 100644
--- a/tests/epoint/test_payment.py
+++ b/tests/epoint/test_payment.py
@@ -1,38 +1,88 @@
+from httpx import Response
+from integrify.epoint.client import EPointClientClass
from integrify.epoint.schemas.parts import TransactionStatus
-from tests.epoint.conftest import TestEPointRequest
+from pytest_mock import MockerFixture
-def test_epoint_payment_request(epoint_mock_payment_response):
- resp = TestEPointRequest(epoint_mock_payment_response).pay(
- amount=1,
- currency='AZN',
- order_id='123456789',
- )
+def test_epoint_payment_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_payment_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch('httpx.Client.request', return_value=epoint_mock_payment_response):
+ resp = epoint_client.pay(
+ amount=1,
+ currency='AZN',
+ order_id='123456789',
+ )
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.redirect_url
- assert resp.body.transaction
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.redirect_url
+ assert resp.body.transaction
-def test_epoint_pay_with_saved_card_request(epoint_mock_pay_with_saved_card_response):
- resp = TestEPointRequest(epoint_mock_pay_with_saved_card_response).pay_with_saved_card(
- amount=1,
- currency='AZN',
- order_id='123456789',
- card_id='card_id',
- )
+def test_epoint_pay_with_saved_card_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_pay_with_saved_card_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_pay_with_saved_card_response,
+ ):
+ resp = epoint_client.pay_with_saved_card(
+ amount=1,
+ currency='AZN',
+ order_id='123456789',
+ card_id='cexxxxxx',
+ )
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.transaction
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.transaction
+def test_epoint_pay_and_save_card_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_pay_and_save_card_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch('httpx.Client.request', return_value=epoint_mock_pay_and_save_card_response):
+ resp = epoint_client.pay_and_save_card(
+ amount=1,
+ currency='AZN',
+ order_id='test',
+ )
-def test_epoint_pay_and_save_card_request(epoint_mock_pay_and_save_card_response):
- resp = TestEPointRequest(epoint_mock_pay_and_save_card_response).pay_and_save_card(
- amount=1,
- currency='AZN',
- order_id='test',
- )
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.transaction
+ assert resp.body.card_id
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.transaction
- assert resp.body.card_id
+
+def test_epoint_payout_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_payout_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch('httpx.Client.request', return_value=epoint_mock_payout_response):
+ resp = epoint_client.payout(
+ amount=1,
+ currency='AZN',
+ order_id='test',
+ card_id='cexxxxxx',
+ )
+
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.transaction
+
+
+def test_epoint_refund_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_refund_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch('httpx.Client.request', return_value=epoint_mock_refund_response):
+ resp = epoint_client.refund(
+ transaction_id='texxxxxx',
+ currency='AZN',
+ )
+
+ assert resp.body.status == TransactionStatus.SUCCESS
diff --git a/tests/epoint/test_split.py b/tests/epoint/test_split.py
index 42cbc71..bcfb1a8 100644
--- a/tests/epoint/test_split.py
+++ b/tests/epoint/test_split.py
@@ -1,46 +1,67 @@
+from httpx import Response
+from integrify.epoint.client import EPointClientClass
from integrify.epoint.schemas.parts import TransactionStatus
-from tests.epoint.conftest import TestEPointRequest
-
-
-def test_epoint_split_payment_request(epoint_mock_split_payment_response):
- resp = TestEPointRequest(epoint_mock_split_payment_response).split_pay(
- amount=100,
- currency='AZN',
- order_id='123456789',
- split_user_id='epoint_user_id',
- split_amount=50,
- )
-
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.redirect_url
-
-
-def test_epoint_split_pay_with_saved_card_request(epoint_mock_split_pay_with_saved_card_response):
- resp = TestEPointRequest(
- epoint_mock_split_pay_with_saved_card_response
- ).split_pay_with_saved_card(
- amount=100,
- currency='AZN',
- order_id='123456789',
- split_user_id='epoint_user_id',
- split_amount=50,
- card_id='cexxxxxx',
- )
-
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.transaction
-
-
-def test_epoint_split_pay_and_save_card_request(epoint_mock_split_pay_and_save_card_response):
- resp = TestEPointRequest(epoint_mock_split_pay_and_save_card_response).split_pay_and_save_card(
- amount=100,
- currency='AZN',
- order_id='123456789',
- split_user_id='epoint_user_id',
- split_amount=50,
- )
-
- assert resp.body.status == TransactionStatus.SUCCESS
- assert resp.body.redirect_url
- assert resp.body.transaction
- assert resp.body.card_id
+from pytest_mock import MockerFixture
+
+
+def test_epoint_split_payment_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_split_payment_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch('httpx.Client.request', return_value=epoint_mock_split_payment_response):
+ resp = epoint_client.split_pay(
+ amount=100,
+ currency='AZN',
+ order_id='123456789',
+ split_user_id='epoint_user_id',
+ split_amount=50,
+ )
+
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.redirect_url
+
+
+def test_epoint_split_pay_with_saved_card_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_split_pay_with_saved_card_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_split_pay_with_saved_card_response,
+ ):
+ resp = epoint_client.split_pay_with_saved_card(
+ amount=100,
+ currency='AZN',
+ order_id='123456789',
+ split_user_id='epoint_user_id',
+ split_amount=50,
+ card_id='cexxxxxx',
+ )
+
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.transaction
+
+
+def test_epoint_split_pay_and_save_card_request(
+ epoint_client: EPointClientClass,
+ epoint_mock_split_pay_and_save_card_response: Response,
+ mocker: MockerFixture,
+):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=epoint_mock_split_pay_and_save_card_response,
+ ):
+ resp = epoint_client.split_pay_and_save_card(
+ amount=100,
+ currency='AZN',
+ order_id='123456789',
+ split_user_id='epoint_user_id',
+ split_amount=50,
+ )
+
+ assert resp.body.status == TransactionStatus.SUCCESS
+ assert resp.body.redirect_url
+ assert resp.body.transaction
+ assert resp.body.card_id
diff --git a/tests/mocks.py b/tests/mocks.py
new file mode 100644
index 0000000..3b8168c
--- /dev/null
+++ b/tests/mocks.py
@@ -0,0 +1,12 @@
+import pytest
+from httpx import Response
+
+from tests.mocks import * # noqa: F403
+
+
+@pytest.fixture(scope='package')
+def test_response():
+ return Response(
+ status_code=200,
+ json={'data1': 'data1', 'data2': 'data2'},
+ )
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..868dc0a
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,119 @@
+import pytest
+from httpx import Response
+from integrify.api import APIClient, APIPayloadHandler
+from integrify.schemas import PayloadBaseModel
+from pydantic import BaseModel
+from pytest_mock import MockerFixture
+
+
+class RequestSchema(PayloadBaseModel):
+ data1: str
+
+
+class ResponseSchema(BaseModel):
+ data1: str
+ data2: str
+
+
+def test_unimplemented_function(api_client: APIClient):
+ with pytest.raises(AttributeError):
+ api_client.login()
+
+
+def test_no_handler(api_client: APIClient, test_response, mocker: MockerFixture):
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=test_response,
+ ):
+ api_client.add_url('test', 'url', 'GET')
+ resp = api_client.test()
+
+ assert isinstance(resp, Response)
+ assert resp.json()['data1'] == 'data1'
+ assert resp.json()['data2'] == 'data2'
+
+
+def test_missing_request_handler_noinput(
+ api_client: APIClient,
+ test_response,
+ mocker: MockerFixture,
+):
+ class Handler(APIPayloadHandler):
+ def __init__(self):
+ super().__init__(None, ResponseSchema)
+
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=test_response,
+ ):
+ api_client.add_url('test', 'url', 'GET')
+ resp = api_client.test()
+ api_client.add_handler('test', Handler)
+
+ assert isinstance(resp, Response)
+ assert resp.json()['data1'] == 'data1'
+ assert resp.json()['data2'] == 'data2'
+
+
+def test_missing_request_handler_input(
+ api_client: APIClient,
+ test_response,
+ mocker: MockerFixture,
+):
+ class Handler(APIPayloadHandler):
+ def __init__(self):
+ super().__init__(None, ResponseSchema)
+
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=test_response,
+ ):
+ api_client.add_url('test', 'url', 'GET')
+ api_client.add_handler('test', Handler)
+
+ with pytest.raises(NotImplementedError):
+ api_client.test(data1='data1')
+
+
+def test_missing_response_handler_input(
+ api_client: APIClient,
+ test_response,
+ mocker: MockerFixture,
+):
+ class Handler(APIPayloadHandler):
+ def __init__(self):
+ super().__init__(RequestSchema, None)
+
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=test_response,
+ ):
+ api_client.add_url('test', 'url', 'GET')
+ api_client.add_handler('test', Handler)
+ resp = api_client.test(data1='data1')
+
+ assert isinstance(resp, Response)
+ assert resp.json()['data1'] == 'data1'
+ assert resp.json()['data2'] == 'data2'
+
+
+def test_with_handlers(
+ api_client: APIClient,
+ test_response,
+ mocker: MockerFixture,
+):
+ class Handler(APIPayloadHandler):
+ def __init__(self):
+ super().__init__(RequestSchema, ResponseSchema)
+
+ with mocker.patch(
+ 'httpx.Client.request',
+ return_value=test_response,
+ ):
+ api_client.add_url('test', 'url', 'GET')
+ api_client.add_handler('test', Handler)
+ resp = api_client.test(data1='data1')
+
+ assert isinstance(resp.body, ResponseSchema)
+ assert resp.body.data1 == 'data1'
+ assert resp.body.data2 == 'data2'