Closed ShahriyarR closed 3 weeks ago
Name | Link |
---|---|
Latest commit | aaa23d40b79f2ea49a891a0bfecd05ba40f1b50e |
Latest deploy log | https://app.netlify.com/sites/integrify-docs/deploys/66f86575d32b140008647d05 |
Deploy Preview | https://deploy-preview-8--integrify-docs.netlify.app |
Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify site configuration.
Bu metod daha compact ve seliqelidi, raziyam, amma sence evvelki variant daha oxunaqli deyil? Her funksiyada birbasha verb, route ve response_type-i gorunur?
save_card hadisəsinin özlüyündə heç xəbəri də olmamalıdır ki, o hara və necə save edir. Hətta onun xəbəri də olmamılıdır ki, nə response qaytaracaq. Ən ideal şəkildə, save_card bir abstraksiyadır. Əvvəlki üsulda save_card özü idarə edir öz state-ini, indiki halda isə xaricdən biz təyin edirik ki, save_card nə etsin(ən azından APİ datanı biz veririk, yenə də shared state update olunur ama ən azından özü qərar vermir ki, nə ilə update edir).
Köhnə:
self.path = env.API.CARD_REGISTRATION
self.verb = 'POST'
self.resp_model = RedirectUrlWithCardIdResponseSchema
Yeni:
api_info = API_DETAILS['save_card']
self.set_path_verb_resp_model(api_info)
İndi biz çöldən API_DETAILS['save_card']-ın içindəki dəyərləri dəyişməklə, request-in hara gedəcəyinə qərar verə bilirik. Əvvəlki halda isə, gedib class-ın metodunu dəyişməli olacaqdıq. Yəni endpoint, post və nə response qaytarmaq detaldır.
Digər bir məsələ də, Epoint-in APİ-sinin support-udu. Deyək ki, Epoint özündə nəsə dəyişiklik elədi və save_card() üçün APİ details dəyişdi. Məcbur gedib bu class-ın bu metodunu dəyişsən o zaman gərək bu lib-in yeni versiyasını da çıxardasan. Amma elə yazılsa ki, çöldən sadəcə hansısa config dəyişmək lazımdı, onda bu lib-i istifadə edənlərin upgrade etməyinə gərək qalmayacaq. SDK-ların ən böyük problemi elə burda başdıyır :) Həmişə broken contract olur 3rd party inteqrasiyalarla.
Bir də mövzu açılmışkən, bu class-da hər metodun shared state-i update etməyinə niyə gərək var? Hər metod instance dəyərləri yeniləyir(self.path, self.verb, self.resp_model) və sonra request atır, buna səbəb nədir?
@ShahriyarR Tutaqki Epoint yeni endpoint əlavə etdi öz docuna. Gərək biz həm API_DETAILS map-a yeni endpointi qeyd etməliyik, üstəgəl EpointRequest classında həmin endpointə aid yeni method yaratmalıyıq. Düşünürəm bu formada 2 qat təkrarçılıq olur hər 1 endpoint üçün.
Bir də mövzu açılmışkən, bu class-da hər metodun shared state-i update etməyinə niyə gərək var? Hər metod instance dəyərləri yeniləyir(self.path, self.verb, self.resp_model) və sonra request atır, buna səbəb nədir?
Evvelki strukturun qurbanidi sadece, evvelki strukture burdan bax: https://github.com/mmzeynalli/integrify/blob/3ac92d159dc33e45eadee67561744eebd39cb3c6/integrify/epoint/sync/misc.py
Ya gerek, call-in ichine gonderim butun argumentleri, ya da shared qoyum, ikinci variant mene daha clean geldi
Yoldaşlar gəlin daha yuxarı səviyyədən baxaq məsələyə. Görürəm ki, integrify ən azı 10 fərqli APİ support-u verən bir SDK-dır. Çox yox gəlin götürək 2 dəstəklədiyimiz ödəniş sistemlərini.
Birincisi indiki halda Epoint-dir.
Epoint endpointləri Enum-da saxlanılır.
class API(StrEnum):
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'
PAY_AND_SAVE_CARD: Literal['/api/1/card-registration-with-pay'] = (
'/api/1/card-registration-with-pay'
)
PAYOUT: Literal['/api/1/refund-request'] = '/api/1/refund-request'
REFUND: Literal['/api/1/reverse'] = '/api/1/reverse'
SPLIT_PAY: Literal['/api/1/split-request'] = '/api/1/split-request'
SPLIT_PAY_WITH_SAVED_CARD: Literal['/api/1/split-execute-pay'] = '/api/1/split-execute-pay'
SPLIT_PAY_AND_SAVE_CARD: Literal['/api/1/split-card-registration-with-pay'] = (
'/api/1/split-card-registration-with-pay'
)
Daha sonra _EPointRequest
(niye protected-dir bu onu anlamadim) adlı bir class var hansı ki, həm konfiqurasiya ilə məşğuldur, həm də faktiki request/response-u idarə edir.
Bu modular həll olmayacaq. Bizə config-i, request/response-u və digər hissələri ayırmaq lazımdır.
Başqa sözlə ifadə etsək bizə lazımdır ki, support verdiyimiz APİ-nin url-lərini ayrıca saxlayaq, həmin url-lərin handler-lərini ayrıca. Sizin SDK-nı install edib istifadə etmək istəyən adam özü hansı inteqrasiyaları aktivləşdirmək istəyirsə, onu da aktivləşdirə bilməlidir, yəni bizim ümumi dizayn həm də pluggable olmalıdır. Yani, SDK 15 inteqrasiya verirse, yalniz mene lazim olan 1 deneni secib aktivleshdire bilmeliyem ama hem de unutmuruq ki, interface eyni olmalidir, yani Epoint-den istifade etmek ucun bir usul, kapitaldan bir usul yaramir SDK-larda.
Fikrimi izah etmek ucun, ordan-burdan tapdigim kod numunelerini bura qeyd edirem, qeyd edim ki, bu mentiqi bir nece usulla dizayn etmek olar. Asagida gostermeye calishdigim, bu usullardan biridir. Kodun coxluguna fikir vermeyin, esas abstraksiyanin duzgun qurulmasidir. Bir defe duzgun qurulandan sonra gerisi cox vaxt, avtomatlashmish shekilde copy+paste olur.
class APISupport:
def __init__(self, name):
self.name = name
self.urls = {} # endpointleri saxlayiriq
self.handlers = {} # her endpoint ucun handle-leri saxlayiriq
def add_url(self, route_name, url):
self.urls[route_name] = url
def add_handler(self, route_name, handler_class):
self.handlers[route_name] = handler_class()
Və daha sonra biz supportunu verdiyimiz həlləri göstəririk:
class EpointSupport(APISupport):
def __init__(self):
super().__init__('Epoint')
self.add_url('PAY', '/api/1/request') # bu data da dependency injection olunsa lap ela, PR-daki dict-den gelse meselen
self.add_handler('PAY', EpointPayHandler)
# Burda bütün dəstəklədiyimiz endpointlər və onların handlerləri
class KapitalAPISupport(APISupport):
def __init__(self):
super().__init__('Other API')
self.add_url('GET_DATA', '/other/api/data')
self.add_handler('GET_DATA', KapitalGetDataHandler)
# ...
Diqqet etsek, her bir APISupport eslinde endpoint ve handlerler ucun "registry" rolunu oynayir.
Bes handler nedir? Handler mehz request ve response-u handle etmek ucundur:
class APIHandler:
def handle_request(self, data=None):
"""Request payload-i Burda duzeldirik"""
return data
def handle_response(self, response):
"""Response-u Burda process edirik"""
return response.json()
EpointPayHandler?
class EpointPayHandler(APIHandler):
def handle_request(self, data=None):
# Request gondermek ucun Nese custom bir mentiq
return data
def handle_response(self, response):
# yene nese custom bir mentiqle response-un bashina oyun aciriq
return response.json()
class KapitalPayHandler(APIHandler):
def handle_request(self, data=None):
# Nese custom bir mentiq
return data
def handle_response(self, response):
# yene nese custom bir mentiq
return response.json()
Bes yaxshi faktiki requestler harda icra olunur? Bunun ucun de bize ayrica executor lazimdir:
import requests
class RequestExecutor:
def __init__(self, api_manager):
self.api_manager = api_manager # az sonra bunu da gosterecem
self.client = requests.Session() # ele qebul edirik ki, requests kitabxanasindan istifade edirik, bunu da inject ede bilerik colden.
def get_url(self, route_name, module_name=None):
return self.api_manager.get_url(route_name, module_name) # bize hansi route lazimdirsa, dinamik gotururuk.
def get_handler(self, route_name, module_name=None):
return self.api_manager.get_handler(route_name, module_name)
def execute_request(self, method, route_name, module_name=None, data=None, headers=None):
url = self.get_url(route_name, module_name) # Epoint-in meslen "PAY" route-unu gotururuk, ama dinamik shekilde
handler = self.get_handler(route_name, module_name) # Epoint-in "PAY" handlerini gotururuk, ama dinamik shekilde
if handler:
data = handler.handle_request(data)
response = self.client.request(method, url, json=data, headers=headers)
# her endpoint ucun evvelceden verb saxlamaqdansa onu arqument sheklinde otururuk amma evvelceden de saxlaya bilerik ferq etmir, sadece bele daha dinamikdir.
if handler:
return handler.handle_response(response)
return response
Fikir verirsinizse, RequestExecutor hec bir shekilde hec bir response kodu yoxlamir, her hansi elave bir ish gormur. Cunki bu onun vezifesi deyil. Eger bize lazimdirsa ki, bizim API-nin requestlerini xususi bir metodla idare edek, o zaman da ishe qarishir spesifik bir APIRequestExecutor:
class APIRequestExecutor(RequestExecutor):
def __init__(self, api_manager):
super().__init__(api_manager) # az sonra manager de gelecek
def execute_api_request(self, method, route_name, module_name=None, data=None, headers=None):
# Request icra olunmamishdan qabaq nese bir xususi mentiq varsa burda bash verir
print(f"Executing {method} request to {route_name} via {module_name}")
response = self.execute_request(method, route_name, module_name, data, headers)
# response-dan sonraki error handling, logging, metrics, tracing ve.s burda bash verir.
if response.status_code != 200:
print(f"Error: {response.status_code}")
return response
Bura qeder yaxshi geldik :) Sual oluna biler ki, be bu boyda hengame harda birleshir? Beli beli, en yuxarida bizde shirin APIManager dayanir, kod sayi cox gorsene biler ama eslinde mentiqi sadedir, supportunu verdiyimiz api-leri register etmek, lazim olanda url ve handleri geri almaq ve bir de hansi nov request executor istifade edecikse onun yadda saxlamaq.
class APIManager(APIModule):
def __init__(self, name='API Manager'):
super().__init__(name)
self.modules = {} # ??? bu nedir?
self.request_executor = None
def add_module(self, module_name, module_class):
module_instance = module_class()
self.modules[module_instance.name] = module_instance # support etdiyimiz inteqrasiyalar burda qeydiyyata alinir
def get_url(self, route_name, module_name=None):
if module_name:
module = self.modules.get(module_name)
if module:
return module.get_url(route_name)
else:
for module in self.modules.values():
url = module.get_url(route_name)
if url:
return url
return None
def add_request_executor(self, executor_class):
self.request_executor = executor_class(self)
def get_handler(self, route_name, module_name=None):
if module_name:
module = self.modules.get(module_name)
if module:
support = module.supports.get(module_name)
if support:
return support.handlers.get(route_name)
else:
for module in self.modules.values():
for support in module.supports.values():
if route_name in support.handlers:
return support.handlers[route_name]
return None
Bes bu modules ne olan sheydir? Modullar da bizim supportunu verdiyimiz API modullardir:
class APIModule:
def __init__(self, name):
self.name = name
self.supports = {}
def add_support(self, support_class):
support_instance = support_class()
self.supports[support_instance.name] = support_instance
def get_url(self, route_name):
for support in self.supports.values():
if route_name in support.urls:
return support.urls[route_name]
Yekunda da ki, hormetli yoldashlar, dushunun ki, bizim istifadeci oz applicationunda, hem Kapitaldan hem de Epoint-den istifade etmek isteyir. Bu zaman onun addimlari bele olacaq
# ilk once APIManager-den istifade edir
api_manager = APIManager()
# Daha sonra lazim olanlari aktivleshdirir
api_manager.add_module('EPOINT', EpointSupport) # modular shekilde register edirik
api_manager.add_module('KAPITAL', KapitalAPISupport)
# request executor-u aktivleshdirir
request_executor = APIRequestExecutor(api_manager)
# Sonra da tesevvur edek ki, goturdu ve EPOINT ucun PAY request gonderdi.
response = request_executor.execute_api_request(
method='POST',
route_name='PAY',
module_name='EPOINT',
data={'amount': 10}
)
# Birini de Kapital ucun gonderdi
response = request_executor.execute_api_request(
method='POST',
route_name='PAY',
module_name='KAPITAL',
data={'amount': 10}
)
SDK yaradicisi olaraq butun arxada bash veren eziyyeti oz uzerimize goturduk ama bunun muqabilinde istifadeciye daha sade bir interface vermish olduq.
Chox gozel izah elemisen, teshekkur, bir nove Factory design patterni xatirlatdi mene. Amma, bir meqam var meni dushundurur. Men bundan elave, sorgulari "funksiyalashdirmaq" isteyirdim ki:
pay(amount, currency, ...)
var, istifadeci ucun fiedlerin izahinacan dokumentasiyasi var. Sen verdiyin struktur super flexible olsa da (xususen de biz maintainerler ucun), ne qeder user-friendlydi gozum tutmadi. Bu strukturu saxlayaraq, funksiyalari elemek mumkundurmu sence? Komp arxasinda deyilem deye test edemmirem, amma bele bir shey:
kapital_request_executor.pay(data)
Pay ozu methodu, apini ve datani formatlayib ishini gorur, yani istifadeci ucun onu da abstractlayiriq.
Yani, bunu yazan developer
response = request_executor.execute_api_request(
method='POST',
route_name='PAY',
module_name='KAPITAL',
data={'amount': 10}
)
birbasha
response = requests.post('api', data={'amount': 10})
etmeyi daha qisa olur
@ShahriyarR Mən inanmıram ölkədə yaradılacaq yeni və ya hazırda olan platformalar eyni anda 2 ödəniş sistemi isitfadə etsin)
@ShahriyarR Mən inanmıram ölkədə yaradılacaq yeni və ya hazırda olan platformalar eyni anda 2 ödəniş sistemi isitfadə etsin)
inanmaq ya inanmamaq, bax esl sual burdadir.
Inanmiriq, chunki meqsed ancaq odenish sistemi deyil, meselen SMS inteqrasiyasi olsa, bir dev hem odenish hem sms istifade ede biler
Chox gozel izah elemisen, teshekkur, bir nove Factory design patterni xatirlatdi mene. Amma, bir meqam var meni dushundurur. Men bundan elave, sorgulari "funksiyalashdirmaq" isteyirdim ki:
- Funksiya adi descriptive olsun
- Gondereceyi datalari bilsin, typeni ve hansilar optionaldi ya yox. Yani indiki structurda:
pay(amount, currency, ...)
var, istifadeci ucun fiedlerin izahinacan dokumentasiyasi var. Sen verdiyin struktur super flexible olsa da (xususen de biz maintainerler ucun), ne qeder user-friendlydi gozum tutmadi. Bu strukturu saxlayaraq, funksiyalari elemek mumkundurmu sence? Komp arxasinda deyilem deye test edemmirem, amma bele bir shey:kapital_request_executor.pay(data)
Pay ozu methodu, apini ve datani formatlayib ishini gorur, yani istifadeci ucun onu da abstractlayiriq.
Yani, bunu yazan developer
response = request_executor.execute_api_request( method='POST', route_name='PAY', module_name='KAPITAL', data={'amount': 10} )
birbasha
response = requests.post('api', data={'amount': 10})
etmeyi daha qisa olur
Factory ile de etmek olar, o zamanda bir novu bir global PaymentGateway yazmaq olar sonra da EpointGateway, KapitalGateway kimi implementasiya elemek olar(ama yene de request/response handlers ve config management out of scope olmalidir class ucun).
Axirda da bir dene PaymentGatewayFactory duzeltmek olar buna benzer bir shey:
class PaymentGatewayFactory:
@staticmethod
def get_payment_gateway(gateway_name):
if gateway_name == 'epoint':
return EpointPaymentGateway()
elif gateway_name == 'kapital':
return KapitalPaymentGateway()
else:
raise ValueError(f"Unsupported payment gateway: {gateway_name}")
Butun bu hadiseleri PaymentSDK-da birleshdirib vermek olar.
class PaymentSDK:
def __init__(self, gateway_name):
self.gateway = PaymentGatewayFactory.get_payment_gateway(gateway_name)
def initialize_payment(self, amount, currency, **kwargs):
return self.gateway.initialize_payment(amount, currency, **kwargs)
def execute_payment(self, payment_id, **kwargs):
return self.gateway.execute_payment(payment_id, **kwargs)
def refund_payment(self, payment_id, amount, **kwargs):
return self.gateway.refund_payment(payment_id, amount, **kwargs)
def get_payment_status(self, payment_id, **kwargs):
return self.gateway.get_payment_status(payment_id, **kwargs)
if __name__ == "__main__":
sdk = PaymentSDK(gateway_name='epoint')
payment_id = sdk.initialize_payment(amount=100, currency='AZN') # ve ya istenilen bashqa bir hadise
sdk.execute_payment(payment_id) # burda da execution ayrica hadisedir
status = sdk.get_payment_status(payment_id) # sonra da statusu goturmek olar
print(status)
Ama bu bele indiki sizin dizaynda deyisiklik teleb edir. Yani her shey mumkundur ama hal-hazirki implementasiya ile yox :) Yene deyirem, maintainer sizsiniz. Eger dushunursunuzse, bu tip hadiseler size lazim deyil, onda PR-i serbest shekilde out of scope deyib baglaya bilersiniz.
Inanmiriq, chunki meqsed ancaq odenish sistemi deyil, meselen SMS inteqrasiyasi olsa, bir dev hem odenish hem sms istifade ede biler
men tam roadmap-i bilmirem, hal-hazirda codebase-de ne gormushem o haqda yazmisham. hetta text-to-pay olsa bele yene de mentiq deyishmir, text-to-pay providerler ucun de eyni qaydada abstraksiya yazmaq olar.
Menim esas meqsedmi butun input ve outputlari type-hinted etmekdi ve request-leri abstraklashdirmaq. Factory metodu ishime uymur, Payriffde artiq funksiya choxdu, Kapitalda voobshe bashqa leveldi, eyni adli funksiyalari saxlamaq hec alinmayacaq. Birinci teklif etdiyin Dependency Injectionu inceleyirem ozum lokalda, xeber ederem bir neche gune
@ShahriyarR muellim, imkan olsa bu struktura baxardin:
Evvel bele bashlamishdiq, sonra 1 request class ve her request function eledik. Mence senin teklif etdiyin strukturu buna apply etmek olar, yox?
Burdaki hell okayi-dir eslinde. https://github.com/mmzeynalli/integrify/blob/3ac92d159dc33e45eadee67561744eebd39cb3c6/integrify/epoint/sync/misc.py
Sadece bir class -> icinde support olunan metodlar daha seliqeli ve asan olacaq, neyinki, her feature-a bir class. Bir daha vurgulayiram ki, SDK-larda xususen de API integrationlarda nezere alinasi hadiselerden bezilerini yaziram:
Indi men size hem de bezi diagramlar verirem ki, vizual olaraq belke daha aydin olar, hem de coxsayli olsa yaxshidi heller.
Əgər functional getmək lazımdırsa o zaman belə bir dizayn iş görər məncə:
graph TD
subgraph Payment_API_Plugins
Epoint
Kapital
end
subgraph Payment_Gateway
PaymentGateway[PaymentGateway Interface]
end
subgraph Payment_API
get_payment_api
end
subgraph Payment_Flow
make_payment
get_transaction_status
save_card
pay_with_saved_card
pay_and_save_card
payout
refund
split_pay
split_pay_with_saved_card
split_pay_and_save_card
end
PaymentGateway --> get_payment_api
get_payment_api --> Epoint
get_payment_api --> Kapital
make_payment --> get_payment_api
get_transaction_status --> get_payment_api
save_card --> get_payment_api
pay_with_saved_card --> get_payment_api
pay_and_save_card --> get_payment_api
payout --> get_payment_api
refund --> get_payment_api
split_pay --> get_payment_api
split_pay_with_saved_card --> get_payment_api
split_pay_and_save_card --> get_payment_api
Daha yuxarıda verdiyim dizayn:
graph TD
classDef classAPI fill:#f9f,stroke:#333,stroke-width:2px;
classDef classHandler fill:#bbf,stroke:#333,stroke-width:2px;
classDef classExecutor fill:#bfb,stroke:#333,stroke-width:2px;
classDef classManager fill:#ffb,stroke:#333,stroke-width:2px;
subgraph API_Support
APISupport
EpointSupport
KapitalAPISupport
end
subgraph API_Handlers
APIHandler
EpointPayHandler
KapitalPayHandler
end
subgraph Request_Execution
RequestExecutor
APIRequestExecutor
end
subgraph API_Management
APIManager
APIModule
end
APISupport --> EpointSupport
APISupport --> KapitalAPISupport
APIHandler --> EpointPayHandler
APIHandler --> KapitalPayHandler
RequestExecutor --> APIRequestExecutor
APIManager --> APIModule
EpointSupport -->|add_url & add_handler| APISupport
KapitalAPISupport -->|add_url & add_handler| APISupport
EpointPayHandler -->|handle_request & handle_response| APIHandler
KapitalPayHandler -->|handle_request & handle_response| APIHandler
RequestExecutor -->|get_url, get_handler, execute_request| APIManager
APIRequestExecutor -->|execute_api_request| RequestExecutor
APIManager -->|add_module, get_url, add_request_executor, get_handler| APIModule
class APISupport classAPI
class EpointSupport classAPI
class KapitalAPISupport classAPI
class APIHandler classHandler
class EpointPayHandler classHandler
class KapitalPayHandler classHandler
class RequestExecutor classExecutor
class APIRequestExecutor classExecutor
class APIManager classManager
class APIModule classManager
Daha sadeleshdirilmish/genishlendirilmish versiyasi:
flowchart TD
subgraph Client
A[Client Code]
end
subgraph SDK
B[PaymentGatewayFactory]
C[PaymentGateway]
D[RequestHandler]
E[ResponseHandler]
F[EpointPaymentGateway]
G[KapitalPaymentGateway]
H[JSONRequestHandler]
I[JSONResponseHandler]
end
A -->|Choose Gateway Type| B
B -->|Get Epoint Gateway| F
B -->|Get Kapital Gateway| G
F -->|Inject Handlers| D
F -->|Inject Handlers| E
G -->|Inject Handlers| D
G -->|Inject Handlers| E
D --> H
E --> I
A -->|Make Payment| C
C -->|Handle Request| D
D -->|Processed Data| C
C -->|Send Data to API| J[External Payment API]
J -->|Response| C
C -->|Handle Response| E
E -->|Processed Response| A
style A fill:#f9f,stroke:#333,stroke-width:4px
style SDK fill:#bbf,stroke:#333,stroke-width:2px
@ShahriyarR Son izahın mükkəmməldi. Bu qədər ətraflı yazdığına görə təşəkkür edirəm. Yazdığların SOLID prinsiplərini xatırlatdı. Açığı hər servis methodunu ayrı class içində yazıb daha sonra dunder call -u çağırmaq mənə çox etik gəlmədi (design olaraq) ona görə qərarlaşıb indiki halına keçirdik. Məqsədimiz əvvəldən sadəcə istədiyimiz servisi rahat işlətmək olub.
Muellim, ortada bir dene chox boyuk problem ordadi ki, payment gatewaylarin APIleri bir birilerinden chooox ferqlenir (ancaq 3une baxmisham hele ozu de, EPoint, Payriff ve Kapital). Save_card Kapitalda yoxdu, Kapitalda 20+ API var ki (bank spesifik) EPoint ve Payriffde yoxdu, EPointde split pay deyilen mentiq var, o biri gatewaylerde yoxdu ve s. Mehz buna gore, senin verdiyin bu strukturunu beynimde qurmaq mene chox chetin gelir.
Istirsen bir doklara nezer sal:
https://docs.payriff.com/ https://epointbucket.s3.eu-central-1.amazonaws.com/files/instructions/API%20Epoint%20en.pdf https://api.birbank.business/products
Gateway mentiqi elesem, Kapitalin unique funksiyasina gore, hem SDK terefde, hem de KapitalImplementation terefde eyniadli yeni funksiya yazmaliyam. Ortaq API chox azdir, olsa da, aldigi datalar ferqlenir.
Bu dizayn ise:
graph TD
classDef classAPI fill:#f9f,stroke:#333,stroke-width:2px;
classDef classHandler fill:#bbf,stroke:#333,stroke-width:2px;
classDef classExecutor fill:#bfb,stroke:#333,stroke-width:2px;
classDef classManager fill:#ffb,stroke:#333,stroke-width:2px;
subgraph API_Support
APISupport
EpointSupport
KapitalAPISupport
end
subgraph API_Handlers
APIHandler
EpointPayHandler
KapitalPayHandler
end
subgraph Request_Execution
RequestExecutor
APIRequestExecutor
end
subgraph API_Management
APIManager
APIModule
end
APISupport --> EpointSupport
APISupport --> KapitalAPISupport
APIHandler --> EpointPayHandler
APIHandler --> KapitalPayHandler
RequestExecutor --> APIRequestExecutor
APIManager --> APIModule
EpointSupport -->|add_url & add_handler| APISupport
KapitalAPISupport -->|add_url & add_handler| APISupport
EpointPayHandler -->|handle_request & handle_response| APIHandler
KapitalPayHandler -->|handle_request & handle_response| APIHandler
RequestExecutor -->|get_url, get_handler, execute_request| APIManager
APIRequestExecutor -->|execute_api_request| RequestExecutor
APIManager -->|add_module, get_url, add_request_executor, get_handler| APIModule
class APISupport classAPI
class EpointSupport classAPI
class KapitalAPISupport classAPI
class APIHandler classHandler
class EpointPayHandler classHandler
class KapitalPayHandler classHandler
class RequestExecutor classExecutor
class APIRequestExecutor classExecutor
class APIManager classManager
class APIModule classManager
ishe yarayir, amma burda da butun type hinting itir ortada, kitabxanani istifade eden developer her sheyi elnen daxil etmeli olur yene.
Əsas odur ki, fikirlərimi qeyd elədim, artıq siz özünüz nə sizə uyğundursa tapıb-tapışdırıb düz-qoş edərsiniz, çünki mən bütün requirementləri bilmirəm :) Bu PR-a ehtiyac yoxdursa, zəhmət olmasa close edərsiniz.
Son bir sheyde fikrini almaq isteyerdim, mumkundurse. Tam dependency injection olmasa da, gateway mentiqine oxhsayir bu:
https://github.com/Adyen/adyen-python-api-library/blob/main/Adyen/__init__.py
Sence bunun kimi, her inteqrasiya ucun SDK classinin ichinde yeni attribute yaratmaq mentiqlidir mi?
muzakirelere ve layihe haqqinda bildiklerimi nezere alib oz fikirlerimi ashagidaki kimi yaza bilerem:
hemde dogru hemde diqqete alinmali meqamlar lazim olduqunu dushunurem:
dogru olanlar:
layihede kodun tekrar isdifadesini ve saxlanilmasini asanlashdira bilmek ucun modularliq ve abstraksiya
ve isdifadechilerin yalniz lazim olan inteqrasiyalari isdifade ede bilmesi ucun pluggable arxitektura
diqqete alinmali meqamlar:
APIManager
, APISupport
, APIHandler
, RequestExecutor
və s.) kodun murekkebliyini ehemiyyetli derecede artiracag ve kodun basha dushulmesini chetinleshdireceknetice: kodu anlamaqda ve deyishiklikler etmekde chetinlikler yarana biler
split_pay
funksiyası)
ozunemexsus xususiyyete mexsusdu. umumi abstraksiyada bu xususiyyetleri effektiv shekilde desteklemek chetin ola bilernetice: gateway-lerin unikalliqi ite biler ve ya bu funksiyalardan tam istifade edile bilmez
elave:
balansli abstraksiya tetbiqi, abstraksiyanı lazimi seviyyede saxlamaq ve kodun oxunaqligini qorumaq vacibdir. elave murekkebliy getirmeden modularligi artirmaq ucun yalniz zeruri abstraksiyadan isdifade etmek daha dogru oldugunu dushunurem.
her bir odenish gateway-i uchun ayri class-lar ve onlarin spesifik method-lari olsa, bu hem gateway-lerin unikalliqlarini destekleyer hemde kodun strukturunu aydilashdirar
kitabxananin esas meqsedi isdifadechiye asan ve effektiv interfeys temin etmekdir. buna gore de, dizayn qerarlari verilerken istifadechi ehtiyyaclari ve sadelik prinsipleri ilk planda olmalidir
menim fikrimce, her bir odenish gateway-i uchun spesifik class-lar ve method-lar yaratmaq ve yalniz zeruri abstraksiya tetbiq etmek daha uygundu. bu hem kodun strukturunu yaxshilashdirar hemde isdifadechilerin kitabxanadan effektiv shekilde istifadesine komek olar
her bir odenish gateway-i uchun ayri class-lar ve onlarin spesifik method-lari olsa, bu hem gateway-lerin unikalliqlarini destekleyer hemde kodun strukturunu aydilashdirar
Yuxarıda hər bir APİ üçün ayrıca class göstərilib, hətta hər biri üçün ayırca request\response handler də yazmaq lazımdır.
APİSupport
interface-dir - EpointAPİSupport ise concrete, APİHanlder interface-dir EPointAPİHandler concrete.
ferqli odenish gateway-leri (meselen, EPoint-in split_pay funksiyası) ozunemexsus xususiyyete mexsusdu. umumi abstraksiyada bu xususiyyetleri effektiv shekilde desteklemek chetin ola biler
Cetin deyil, EpointAPİSupport-da split_pay-i qeyd etmek lazimdir.
kitabxananin esas meqsedi isdifadechiye asan ve effektiv interfeys temin etmekdir. buna gore de, dizayn qerarlari verilerken istifadechi ehtiyyaclari ve sadelik prinsipleri ilk planda olmalidir
Raziyam. Odur ki, en bele gozel halda Hyperswitch-e baxmaq olar. https://docs.hyperswitch.io/learn-more/hyperswitch-architecture
Hyperswitch demək olar ki, bilinən hər şeyə support verir: https://github.com/juspay/hyperswitch
@ShahriyarR indiki strukturda neyi beyenmirsen tam olaraq? DI falan yoxdu raziyam, amma:
Amma yene de heveslenib, gece 2nin yarisi bele bir shey yazdim, haminin fikri maraqlidi: @ShahriyarR @vahidzhe @rashadseyfulla
Demeli benzer struktur, ayrica request executor etmirem, type hintingi qorumaq ucun:
class APISupport:
def __init__(self, name):
self.name = name
self.url = ''
self.urls = {} # endpointleri saxlayiriq
self.handlers: dict[str, APIHandler] = {} # her endpoint ucun handle-leri saxlayiriq
self.default_handler: Optional[APIHandler] = None
self.client = httpx.Client(timeout=10)
def add_url(self, route_name, url):
self.urls[route_name] = url
def add_handler(self, route_name, handler_class):
self.handlers[route_name] = handler_class()
def req(self, url, handler: Optional['APIHandler'], *args, **kwds):
if handler:
data = handler.handle_request(**kwds)
response = self.client.request('POST', url, json=data)
if handler:
return handler.handle_response(response)
return response
def __getattribute__(self, name: str) -> Any:
try:
return super().__getattribute__(name)
except AttributeError:
if name not in self.urls:
raise
url = self.url + self.urls[name]
handler = self.handlers.get(name, self.default_handler)
return lambda *args, **kwds: self.req(url, handler, *args, **kwds)
class APIHandler:
def handle_request(self, *args, **kwds):
"""Request payload-i Burda duzeldirik"""
raise NotImplementedError
def handle_response(self, response: httpx.Response):
"""Response-u Burda process edirik"""
return response.json()
Bundan sonra, daha concrete olan EPoint:
class EPointBaseHandler(APIHandler):
def handle_request(self, **kwds):
print(env.EPOINT_PUBLIC_KEY)
data = {'public_key': env.EPOINT_PUBLIC_KEY, 'language': env.EPOINT_INTERFACE_LANG, **kwds}
b64data = base64.b64encode(json.dumps(data).encode()).decode()
return {
'data': b64data,
'signature': generate_signature(b64data),
}
# Yeni handler-e ehtiyac olsa, datani lazimi formata salib, super() ile BaseHandleri yeniden chagira bilerik
class EpointSupport(APISupport):
def __init__(self):
super().__init__('Epoint')
self.url = 'https://epoint.az'
self.default_handler = EPointBaseHandler()
self.add_url('get_transaction_status', '/api/1/get-status')
# Burda bütün dəstəklədiyimiz endpointlər və onların handlerləri
if TYPE_CHECKING:
def get_transaction_status(self, transaction: str):
pass
If type cheking hissesi bize type hintinge icaze verir. Eslinde o funksiyalar movcud deyil, ve __get_attribute__
terefinden icra olunur lazimi hisseler. Yani bu kod ishleyir, test elemishem:
print(EpointSupport().get_transaction_status(transaction='te002299644'))
Indi gencler ne dushunursuz, fikirleriniz chox maraqlidi
Düşünürəm ki, indiki kod bazasını müvəqqəti dondurub, qəşəng bir Design Doc yazmaq lazımdır. Daha sonra da o Design Doc-u umumi qrupda muzakireye cixarmaq olar. Icinde, SDK-ya nece inteqrasiya olunmalidir Gateway-ler onun flow-su, diagrami olmalidir. Daha sonra User Flow vermek olar ki, istifadeci neler etmelidir ki, istifade etsin, ve.s Hansi nov arxitekturadan istifade olunacaq, niye olunacaq bunlari da qeyd etmek lazimdir. Mumkun mertebe detalli, diagramli, arxitekturali. Bu size dehset cox ish kimi gorsene biler ama umumen qayda beledir ki, 1 ay planning 8-10 ay developmenti save edir.
Ireli vaxtlarda da her hansi feature cixardanda onun PRD(Product Requirement Doc)-sinin yazib hazirlamaq lazimdir, sonra butun requirementleri toplamaq ve en sonda developmente bashlamaq.
O ki qaldi yuxaridaki kod numunesine, menim ucun Explicit is better than Implicit. Python Descriptor-larla(__getattribute__
, __getattr__
) oynamaq kodu daha qeliz edir. Sonra kimise onboard elemek daha cetin olur.
@ShahriyarR Design Docla bagli example-in varsa bashqa proyektlerden (simple olsa lap gozel), bolushsen ela olardi.
O ki qaldi, __getattr__
, o sadece "movcud olmayan" funksiyalari icra etmek ucundu. eks halda taftalogiya olacaq:
class EpointSupport(APISupport):
def __init__(self):
super().__init__('Epoint')
self.url = 'https://epoint.az'
self.default_handler = EPointBaseHandler()
self.add_url('pay', '/api/1/get-status')
self.add_url('get_transaction_status', '/api/1/get-status')
# Burda bütün dəstəklədiyimiz endpointlər və onların handlerləri
def get_transaction_status(self, transaction: str):
return self.req('get_transaction_status', transaction=transaction)
def pay(self, amount: Decimal, currency: str, order_id: str):
return self.req('pay'. amount=amount, currency=currency, order_id=order_id)
Chox tekrarlanma olur. __getattr__
sadece funksiyanin adina uygun (note: funksiyanin adi, url ve handler name ile eynidir, ve sorgulanan attributun dict-de olub olmamasi yoxlanir) goturur, ve datani *args
ve **kwargs
kimi handlere oturub, sorgunu ata bilir. Bu mence base classda CEMI BIR DEFE edilecek bir sheydir, ve hec bir subclassda override olunmayacaq.
@ShahriyarR bir de ortalarda qeyd etmishdim, bu var:
https://github.dev/Adyen/adyen-python-api-library/Adyen/adyen-python-api-library/Adyen/__init__.py
Burda client obshi class-dadi, her integrationa dependency injected olub. Factory-ye benzer, amma
def get_payment_gateway(gateway_name):
if gateway_name == 'epoint':
return EpointPaymentGateway()
elif gateway_name == 'kapital':
return KapitalPaymentGateway()
else:
raise ValueError(f"Unsupported payment gateway: {gateway_name}")
yox,
class Integrify:
def __init__():
self.client = httpx.Client(timeout=10)
self.epoint = EPointClient(self.client)
self.kapital = KapitalClient(self.client)
edirik. Bu struktur ne qeder duzdur amma emin deyilem, mehshur odeme sistemidi, onlarin oz official kitabxanasidi.
@ShahriyarR bir de ortalarda qeyd etmishdim, bu var:
https://github.dev/Adyen/adyen-python-api-library/Adyen/adyen-python-api-library/Adyen/__init__.py
Burda client obshi class-dadi, her integrationa dependency injected olub. Factory-ye benzer, amma
def get_payment_gateway(gateway_name): if gateway_name == 'epoint': return EpointPaymentGateway() elif gateway_name == 'kapital': return KapitalPaymentGateway() else: raise ValueError(f"Unsupported payment gateway: {gateway_name}")
yox,
class Integrify: def __init__(): self.client = httpx.Client(timeout=10) self.epoint = EPointClient(self.client) self.kapital = KapitalClient(self.client)
edirik. Bu struktur ne qeder duzdur amma emin deyilem, mehshur odeme sistemidi, onlarin oz official kitabxanasidi.
Adyen-deki ele Factory kimi bir sheydi, sadece o butun funksionalligi verir. Cunki Adyen cemi 1 processingdi ve eyni client-i istifade edir. AdyenClient eslinde Auth-du burda bele basha dushdum ki, ve cox boyuk ehtimal butun funksionalliq bir key/token-le gedir.
Burda ise biz multiple support veririk. Yani yalniz Epointin tokenini elde eden adam, bele cixir ki, hem de butun diger support verilenlerin key-lerini vermelidir? Elemek olar eslinde ki, hansi aktivdirse o da istifade olunsun, env variable-larla. Sadece men ele dushunurem ki, istifadeci secse ki, neyi aktivleshdirmelidir ona daha serf eder, neyinki her sheyi umumi goturse.
class Integrify:
def __init__():
self.client = httpx.Client(timeout=10) # <- yalniz httpx client-dan getmir da sohbet burda yeqin ki, hardasa bu auth token de vermeliyik
self.epoint = EPointClient(self.client) # <- epoint token
self.kapital = KapitalClient(self.client) # <- kapital token
Eger bu usulla getsek, configure methodu qoya bilerik her inteqrasiyaya, hansi ki, lazim olan auth/headerleri onceden one-time configlesin. Yani istifade edende:
integrify = Integrify()
integrify.epoint.configure(public_key, private_key) # ya da env-de set ele, ordan oxusun lib
integrify.epoint.pay(data)
Amma duzun desem, evvelki usul (__get_attr__
) mence daha dev-friendly gorunur. Istifadeci terefden de, lazim olan integrasiyani import edib ishledecek. Indi danishdigimiz metodda configure
funksiyasini hell etsek de, sen demish butun integrasiyalari "istifade" etdirmish olacayiq
Changes: