mmzeynalli / integrify

Integrify API inteqrasiyalarını rahatlaşdıran sorğular kitabaxanasıdır.
https://integrify.mmzeynalli.dev/
GNU General Public License v3.0
21 stars 2 forks source link

refactor: use map for managing the path verb and responce model #8

Closed ShahriyarR closed 3 weeks ago

ShahriyarR commented 1 month ago

Changes:

netlify[bot] commented 1 month ago

Deploy Preview for integrify-docs ready!

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...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

ShahriyarR commented 1 month ago

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?

vahidzhe commented 1 month ago

@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.

mmzeynalli commented 1 month ago

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

ShahriyarR commented 1 month ago

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.

mmzeynalli commented 1 month ago

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:

  1. Funksiya adi descriptive olsun
  2. 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

vahidzhe commented 1 month ago

@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 commented 1 month ago

@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.

mmzeynalli commented 1 month ago

Inanmiriq, chunki meqsed ancaq odenish sistemi deyil, meselen SMS inteqrasiyasi olsa, bir dev hem odenish hem sms istifade ede biler

ShahriyarR commented 1 month ago

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:

  1. Funksiya adi descriptive olsun
  2. 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.

ShahriyarR commented 1 month ago

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.

mmzeynalli commented 1 month ago

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

mmzeynalli commented 1 month ago

@ShahriyarR muellim, imkan olsa bu struktura baxardin:

https://github.com/mmzeynalli/integrify/blob/3ac92d159dc33e45eadee67561744eebd39cb3c6/integrify/epoint/sync/misc.py

Evvel bele bashlamishdiq, sonra 1 request class ve her request function eledik. Mence senin teklif etdiyin strukturu buna apply etmek olar, yox?

ShahriyarR commented 1 month ago

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
vahidzhe commented 1 month ago

@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.

mmzeynalli commented 1 month ago

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.

ShahriyarR commented 1 month ago

Ə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.

mmzeynalli commented 1 month ago

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?

rashadseyfulla commented 1 month ago

muzakirelere ve layihe haqqinda bildiklerimi nezere alib oz fikirlerimi ashagidaki kimi yaza bilerem:

hemde dogru hemde diqqete alinmali meqamlar lazim olduqunu dushunurem:

dogru olanlar:

  1. layihede kodun tekrar isdifadesini ve saxlanilmasini asanlashdira bilmek ucun modularliq ve abstraksiya

  2. ve isdifadechilerin yalniz lazim olan inteqrasiyalari isdifade ede bilmesi ucun pluggable arxitektura

diqqete alinmali meqamlar:

  1. teklif olunan abstraksiya tebeqeleri (APIManager, APISupport, APIHandler, RequestExecutor və s.) kodun murekkebliyini ehemiyyetli derecede artiracag ve kodun basha dushulmesini chetinleshdirecek

netice: kodu anlamaqda ve deyishiklikler etmekde chetinlikler yarana biler

  1. ferqli odenish gateway-leri (meselen, EPoint-in split_pay funksiyası) ozunemexsus xususiyyete mexsusdu. umumi abstraksiyada bu xususiyyetleri effektiv shekilde desteklemek chetin ola biler

netice: gateway-lerin unikalliqi ite biler ve ya bu funksiyalardan tam istifade edile bilmez

elave:

  1. 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.

  2. 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

  3. 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

ShahriyarR commented 1 month ago

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

mmzeynalli commented 1 month ago

@ShahriyarR indiki strukturda neyi beyenmirsen tam olaraq? DI falan yoxdu raziyam, amma:

  1. Type hinting var, her funksiya ne alir ne qaytarir deqiqdi (kitabxananin esas megzi)
  2. Teze API olsa, EPointRequest-in ichinde yeni funksiyadi, URL deyishse uygun funksiyada URLi deyishirik, data formati deyishse, response modeli deyishirik ve s. Bura seni narahat edir anladim, amma nesi bilmedim. Abstraktlashdirmaga ne gerek var ki?
  3. Hec bir API Integration o birine oxshamayacaq. Copy/paste-lik kod yaratmaq mene hele de surreal gorunur. Senin teklif etdiyn kodlar eksine kod bazasini artiracaq, inteqrasiyalar bir birinden chox ferqlenir deye.
  4. Usere dynamiclik vermek fikrim yoxdu duzu, ki ozu pluggable ishler gore bilsin. Mence o artiqdi ve gereksizdi, isteyen birbasha request atar. Meqsed istifadechi ucun API ve request datani abstraktlashdirmaqdi.
  5. Rust kodunu oxuyammadigim ucun Hyperswitchle bagli bir fikir bildiremmeyecem
  6. Her inteqrasiya ucun nese ferqli olsa, easily override ede bilirem funksiyalari
mmzeynalli commented 1 month ago

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

ShahriyarR commented 1 month ago

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.

mmzeynalli commented 1 month ago

@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.

mmzeynalli commented 1 month ago

@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 commented 1 month ago

@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
mmzeynalli commented 1 month ago

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

mmzeynalli commented 3 weeks ago

15 de davam ede bilerik