From 0766fb0773ae15f5c6d4f5006753aa12160db827 Mon Sep 17 00:00:00 2001 From: Sivadas Date: Sun, 15 Oct 2023 22:00:52 +0530 Subject: [PATCH 1/3] basel level done --- README.md | 200 +++++++++++++++++++- examples/generate_invoice.py | 69 +++++++ examples/onboard_egs.py | 41 +++++ setup.py | 26 +++ src/flick/__init__.py | 2 + src/flick/api_service.py | 102 +++++++++++ src/flick/bills.py | 345 +++++++++++++++++++++++++++++++++++ 7 files changed, 783 insertions(+), 2 deletions(-) create mode 100644 examples/generate_invoice.py create mode 100644 examples/onboard_egs.py create mode 100644 setup.py create mode 100644 src/flick/__init__.py create mode 100644 src/flick/api_service.py create mode 100644 src/flick/bills.py diff --git a/README.md b/README.md index 1fc000d..ea77c96 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,198 @@ -# flick-python-sdk -Flick's API SDK for Python +# Flick Python SDK +![Platform](https://img.shields.io/badge/python-3-blue) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) + + +A python interface for interacting with the APIs of Flick. + +- [Installation](#installation) +- [Getting Started](#getting-started) +- [Documentation](#documentation) +- [Examples](#examples) +- [Contribute to our SDK](#contributing) +- [License](#license) +- [Support](#support) + +## Installation +To use the Flick Python SDK in your project, you can install it via pip: + +```bash +pip install flick-python-sdk +``` + +## Getting Started +Before using the package, you need to configure it with your API credentials. You should have an apiKey and specify whether you are using the 'sandbox' or 'production' environment. + +Here's how you to initiate our SDK in your project: + +```python +from flick import Config +from bills import Bills + +config = Config('sandbox', 'your-api-key') +api = Bills(config=config) +``` + +## Documentation +To learn about available methods and their usage, please refer to the [official API documentation](https://docs.flick.network/). +Here's a glimpse to our Bills Module: + +### Bills Client +The Bills client provides access to various functionalities for managing bills. You can interact with the following API endpoints: + +#### Onboard EGS to ZATCA: + +```python +egs_data = { /* Your EGS data - Check Documentation */ } +response = api.onboard_egs(egsData) +``` + +#### Compliance Check: + +```python +egs_uuid = 'your-egs-uuid'; +response = api.do_compliance_check(egs_uuid); +``` + +#### Generate E-Invoice for Phase-2 in Saudi Arabia: +```python +invoiceData = { /* Your invoice data - Check Documentation */ }; +response = api.generate_invoice(invoiceData); +``` + +## Examples + +1. Here's an Example of how you can **onboard multiple EGS to ZATCA Portal** [If you are onboarding PoS devices or VAT-Group members, this comes handy]. + +```python +from flick.api_service import Config +from flick.bills import Bills,EGSData,Devices + +config = Config('sandbox', 'your-api-key') + +client = Bills(config) + +egs_data = EGSData( + vat_name='Test Co.', + vat_number='300000000000003', + devices=[ + Devices( + device_name='TestEGS1', + city='Riyadh', + city_subdiv='Test Dist.', + street='Test St.', + plot='1234', + building='1234', + postal='12345', + # This will be 10-digit TIN if you are onboarding a VAT-Group Member + branch_name='Riyad Branc h 1', + branch_industry='Retail', + otp='123321', + ), Devices( + device_name='TestEGS2', + city='Riyadh', + city_subdiv='Test Dist.', + street='Test St.', + plot='1234', + building='1234', + postal='12345', + # This will be 10-digit TIN if you are onboarding a VAT-Group Member + branch_name='Riyad Branch 1', + branch_industry='Retail', + otp='123321', + ), + ] +) + +response = client.onboard_egs(egs_data=egs_data) +print(response.text) + +``` + +2. Here's an Example of how you can **Genereate a ZATCA-Complied E-Invoice**. + +```python +from flick.api_service import Config +from flick.bills import Bills, InvoiceData, PartyAddId, PartyDetails, AdvanceDetails, AdvanceInvoices, Invoice, LineItems + +config = Config('sandbox', 'your-api-key') + +client = Bills(config) +invoice_data = InvoiceData( + egs_uuid='7b9cc231-0e14-4bff-938c-4603fe10c4bc', + invoice_ref_number='INV-5', + issue_date='2023-01-01', + issue_time='01=40=40', + party_details=PartyDetails( + party_name_ar='شركة اختبار', + party_vat='300001111100003', + party_add_id=PartyAddId( + crn=45463464 + ), + city_ar='جدة', + city_subdivision_ar='حي الشرفية', + street_ar='شارع الاختبار', + plot_identification='1234', + building='1234', + postal_zone='12345', + ), + doc_type='388', + inv_type='standard', + payment_method=10, + currency='SAR', + total_tax=142., + has_advance=True, + advance_details=AdvanceDetails( + advance_amount=575, + total_amount=2875, + advance_invoices=[ + AdvanceInvoices( + tax_category='S', + tax_percentage=0.15, + taxable_amount=500, + tax_amount=75, + invoices=[ + Invoice( + invoice_id='INV-1', + issue_date='2022-12-10', + issue_time='12=28=17', + ), + ], + ), + ], + ), + lineitems=[ + LineItems( + name_ar='متحرك', + quantity=1, + tax_category='S', + tax_exclusive_price=750, + tax_percentage=0.15, + ), + LineItems( + name_ar='حاسوب محمول', + quantity=1, + tax_category='S', + tax_exclusive_price=1750, + tax_percentage=0.15, + ), + ], +) + +response = client.generate_invoice(invoice_data=invoice_data) +print(response.text) + + +``` + +## Contributing + +We welcome contributions from the community. If you find issues or have suggestions for improvements, please open an issue or create a pull request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Support + +If you encounter any issues or have questions, please contact our support team at support@flick.network \ No newline at end of file diff --git a/examples/generate_invoice.py b/examples/generate_invoice.py new file mode 100644 index 0000000..7a09b6a --- /dev/null +++ b/examples/generate_invoice.py @@ -0,0 +1,69 @@ +from flick.api_service import Config +from flick.bills import Bills, InvoiceData, PartyAddId, PartyDetails, AdvanceDetails, AdvanceInvoices, Invoice, LineItems + +config = Config('sandbox', 'your-api-key') + +client = Bills(config) +invoice_data = InvoiceData( + egs_uuid='7b9cc231-0e14-4bff-938c-4603fe10c4bc', + invoice_ref_number='INV-5', + issue_date='2023-01-01', + issue_time='01=40=40', + party_details=PartyDetails( + party_name_ar='شركة اختبار', + party_vat='300001111100003', + party_add_id=PartyAddId( + crn=45463464 + ), + city_ar='جدة', + city_subdivision_ar='حي الشرفية', + street_ar='شارع الاختبار', + plot_identification='1234', + building='1234', + postal_zone='12345', + ), + doc_type='388', + inv_type='standard', + payment_method=10, + currency='SAR', + total_tax=142., + has_advance=True, + advance_details=AdvanceDetails( + advance_amount=575, + total_amount=2875, + advance_invoices=[ + AdvanceInvoices( + tax_category='S', + tax_percentage=0.15, + taxable_amount=500, + tax_amount=75, + invoices=[ + Invoice( + invoice_id='INV-1', + issue_date='2022-12-10', + issue_time='12=28=17', + ), + ], + ), + ], + ), + lineitems=[ + LineItems( + name_ar='متحرك', + quantity=1, + tax_category='S', + tax_exclusive_price=750, + tax_percentage=0.15, + ), + LineItems( + name_ar='حاسوب محمول', + quantity=1, + tax_category='S', + tax_exclusive_price=1750, + tax_percentage=0.15, + ), + ], +) + +response = client.generate_invoice(invoice_data=invoice_data) +print(response.text) diff --git a/examples/onboard_egs.py b/examples/onboard_egs.py new file mode 100644 index 0000000..b965408 --- /dev/null +++ b/examples/onboard_egs.py @@ -0,0 +1,41 @@ +from flick.api_service import Config +from flick.bills import Bills,EGSData,Devices + +config = Config('sandbox', 'your-api-key') + +client = Bills(config) + +egs_data = EGSData( + vat_name='Test Co.', + vat_number='300000000000003', + devices=[ + Devices( + device_name='TestEGS1', + city='Riyadh', + city_subdiv='Test Dist.', + street='Test St.', + plot='1234', + building='1234', + postal='12345', + # This will be 10-digit TIN if you are onboarding a VAT-Group Member + branch_name='Riyad Branc h 1', + branch_industry='Retail', + otp='123321', + ), Devices( + device_name='TestEGS2', + city='Riyadh', + city_subdiv='Test Dist.', + street='Test St.', + plot='1234', + building='1234', + postal='12345', + # This will be 10-digit TIN if you are onboarding a VAT-Group Member + branch_name='Riyad Branch 1', + branch_industry='Retail', + otp='123321', + ), + ] +) + +response = client.onboard_egs(egs_data=egs_data) +print(response.text) \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0b252de --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup,find_packages +with open("README.md", "r", encoding = "utf-8") as fh: + long_description = fh.read() + +setup( + name = "flick-python-sdk", + version = "0.0.13", + author = "Sivadas Rajan", + author_email = "sivadasrajan@gmail.com", + description = "A Python wrapper for interacting with APIs from Flick.", + long_description = long_description, + long_description_content_type = "text/markdown", + install_requires=["requests>=2.25.0"], + url = "https://test.pypi.org/project/flick-python-sdk/", + project_urls = { + "Bug Tracker": "https://github.com/flick-network/flick-python-sdk/issues", + }, + classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + package_dir = {"": "src"}, + packages = ['flick'], + python_requires = ">=3.6" +) diff --git a/src/flick/__init__.py b/src/flick/__init__.py new file mode 100644 index 0000000..ba7e9c7 --- /dev/null +++ b/src/flick/__init__.py @@ -0,0 +1,2 @@ +from .api_service import * +from .bills import * diff --git a/src/flick/api_service.py b/src/flick/api_service.py new file mode 100644 index 0000000..22b8fea --- /dev/null +++ b/src/flick/api_service.py @@ -0,0 +1,102 @@ +""" Module for initializing request class """ +import urllib +import requests + + +class InvalidEnvironmentException(Exception): + """ For raising invalid environment exceptions """ + + +class Config(): + + """ Config class for passing the api key and selecting between """ + + SANDBOX = 'sandbox' + PRODUCTION = 'production' + + def __init__(self, environment: str, api_key: str) -> None: + if environment == self.SANDBOX or environment == self.PRODUCTION: + self.environment = environment + self.api_key = api_key + else: + raise InvalidEnvironmentException( + "Invalid environment type. use 'sandbox' or 'production'") + + +class FlickAPI(requests.Session): + """Base class for making all requsts to flick.network""" + + SANDBOX_BASE_URL = "https://sandbox-api.flick.network" + PRODUCTION_BASE_URL = "https://api.flick.network" + + def get_base_url(self): + """ Get the base url for making requests """ + + if self.custom_config.environment == Config.PRODUCTION: + return self.PRODUCTION_BASE_URL + + return self.SANDBOX_BASE_URL + + + + def __init__(self, *args, config: Config, **kwargs): + """ + Customised requests.Session class for common base url + + Args: + url_base (string, optional): base url for all web requests . Defaults to None. + """ + + self.custom_config = config + super(FlickAPI, self).__init__(*args, **kwargs) + self.url_base = self.get_base_url() + + + + def request(self, method, url, + params=None, + data=None, + headers=None, + cookies=None, + files=None, + auth=None, + timeout=None, + allow_redirects=True, + proxies=None, + hooks=None, + stream=None, + verify=None, + cert=None, + json=None,): + """concatenate and create all relavent urls + + Args: + method (str): HTTP Method + url (str): Remaining part of URL + + Returns: + requests: requests object to use + """ + modified_url = urllib.parse.urljoin(base=self.url_base, url=url) + + if headers is not None: + headers["Authorization"] = f"Bearer {self.custom_config.api_key}", + else: + headers = { + "Authorization": f"Bearer {self.custom_config.api_key}", + } + + return super(FlickAPI, self).request(method, modified_url, params, + data, + headers, + cookies, + files, + auth, + timeout, + allow_redirects, + proxies, + hooks, + stream, + verify, + cert, + json) diff --git a/src/flick/bills.py b/src/flick/bills.py new file mode 100644 index 0000000..ad6d419 --- /dev/null +++ b/src/flick/bills.py @@ -0,0 +1,345 @@ +import json +from typing import List +from .api_service import FlickAPI, Config + + +class Devices(): + """ The Devices class. """ + + def __init__(self, device_name: str, city: str, city_subdiv: str, street: str, plot: str, building: str, postal: str, branch_name: str, branch_industry: str, otp: str): + """ Initializes an instance of Devices. """ + self.device_name = device_name + self.city = city + self.city_subdiv = city_subdiv + self.street = street + self.plot = plot + self.building = building + self.postal = postal + self.branch_name = branch_name + self.branch_industry = branch_industry + self.otp = otp + + +class EGSData: + """ The EGSData class. """ + + def __init__(self, vat_name: str, vat_number: str, devices: List[Devices]): + """ + Initializes an instance of EGSData. + + Args: + vat_name (str): The VAT name. + vat_number (str): The VAT number. + devices (List[Devices]): A list representing devices. + """ + self.vat_name = vat_name + self.vat_number = vat_number + self.devices = devices + + def to_json(self): + + out = { + "vat_name": self.vat_name, + "vat_number": self.vat_number, + } + devices = [] + for device in self.devices: + devices.append(device.__dict__) + out["devices"] = devices + + return out + + +class PartyAddId: + """ The PartyAddId class. """ + + def __init__(self, crn: int): + self.crn = crn + + +class PartyDetails: + """ The PartyDetails class. """ + + def __init__(self, + party_name_ar: str, + party_vat: str, + city_ar: str, + city_subdivision_ar: str, + street_ar: str, + postal_zone: str, + party_add_id: PartyAddId = None, + city_en: str = None, + city_subdivision_en: str = None, + street_en: str = None, + plot_identification: str = None, + building: str = None, + party_name_en: str = None, + ): + self.party_name_ar = party_name_ar + self.party_name_en = party_name_en + self.party_vat = party_vat + self.party_add_id = party_add_id + self.city_ar = city_ar + self.city_en = city_en + self.city_subdivision_ar = city_subdivision_ar + self.city_subdivision_en = city_subdivision_en + self.street_ar = street_ar + self.street_en = street_en + self.plot_identification = plot_identification + self.building = building + self.postal_zone = postal_zone + def to_json(self): + + out = { + "party_name_ar" : self.party_name_ar, + "party_vat" : self.party_vat, + "city_ar" : self.city_ar, + "city_subdivision_ar" : self.city_subdivision_ar, + "street_ar" : self.street_ar, + "postal_zone" : self.postal_zone, + "party_add_id" : self.party_add_id, + "city_en" : self.city_en, + "city_subdivision_en" : self.city_subdivision_en, + "street_en" : self.street_en, + "plot_identification" : self.plot_identification, + "building" : self.building, + "party_name_en" : self.party_name_en, + } + + out["party_add_id"] = self.party_add_id.__dict__ + + return out + + +class Invoice: + """ The Invoice class. """ + + def __init__(self, invoice_id: str, issue_date: str, issue_time: str): + self.id = invoice_id + self.issue_date = issue_date + self.issue_time = issue_time + + +class LineItems: + """ The LineItems class. """ + + def __init__(self, + name_ar: str, + quantity: float, + tax_category: str, + tax_exclusive_price: float, + tax_percentage: float, + name_en: str = None, + ): + self.name_ar = name_ar + self.name_en = name_en + self.quantity = quantity + self.tax_category = tax_category + self.tax_exclusive_price = tax_exclusive_price + self.tax_percentage = tax_percentage + + +class AdvanceInvoices: + """ The AdvanceInvoices class. """ + + def __init__(self, + tax_category: str, + tax_percentage: float, + taxable_amount: float, + tax_amount: float, + invoices: List[Invoice],): + self.tax_category = tax_category + self.tax_percentage = tax_percentage + self.taxable_amount = taxable_amount + self.tax_amount = tax_amount + self.invoices = invoices + + def to_json(self): + + out = { + "tax_category": self.tax_category, + "tax_percentage": self.tax_percentage, + "taxable_amount": self.taxable_amount, + "tax_amount": self.tax_amount, + } + invoices = [] + for invoice in self.invoices: + invoices.append(invoice.__dict__) + out["invoices"] = invoices + + return out + + +class AdvanceDetails: + """ The AdvanceDetails class. """ + + def __init__(self, + advance_amount: float, + total_amount: float, + advance_invoices: AdvanceInvoices + ): + self.advance_amount = advance_amount + self.total_amount = total_amount + self.advance_invoices = advance_invoices + + def to_json(self): + + out = { + "advance_amount":self.advance_amount, + "total_amount":self.total_amount, + } + advance_invoices = [] + for advance_invoice in self.advance_invoices: + advance_invoices.append(advance_invoice.to_json()) + out["advance_invoices"] = advance_invoices + + return out + + +class InvoiceData: + """ The EGSData class. """ + + def __init__(self, + egs_uuid: str, + invoice_ref_number: str, + issue_date: str, + issue_time: str, + doc_type: str, + inv_type: str, + payment_method: int, + lineitems: LineItems, + party_details: PartyDetails, + advance_details: AdvanceDetails = None, + has_advance: bool = None, + currency: str = None, + total_tax: str = None,): + self.egs_uuid = egs_uuid + self.invoice_ref_number = invoice_ref_number + self.issue_date = issue_date + self.issue_time = issue_time + self.party_details = party_details + self.doc_type = doc_type + self.has_advance = has_advance + self.advance_details = advance_details + self.inv_type = inv_type + self.payment_method = payment_method + self.currency = currency + self.total_tax = total_tax + self.lineitems = lineitems + + def to_json(self): + + out = { + "egs_uuid" : self.egs_uuid, + "invoice_ref_number" : self.invoice_ref_number, + "issue_date" : self.issue_date, + "issue_time" : self.issue_time, + "doc_type" : self.doc_type, + "inv_type" : self.inv_type, + "payment_method" : self.payment_method, + "has_advance" : self.has_advance, + "currency" : self.currency, + "total_tax" : self.total_tax, + } + out["party_details"] = self.party_details.to_json() + lineitems = [] + for lineitem in self.lineitems: + lineitems.append(lineitem.__dict__) + out["lineitems"] = lineitems + out["advance_details"] = self.advance_details.to_json() + + return out + + +class Bills: + """ + A class for handling billing operations using the Flick API. + + Args: + config (Config): A configuration object for the Flick API. + + Attributes: + api (FlickAPI): An instance of the FlickAPI class with the provided configuration. + + Methods: + - onboardEGS(egsData: EGSData) -> dict: + Onboard an EGS with the provided data. + + - doComplianceCheck(egs_uuid: str) -> dict: + Perform a compliance check for an EGS with the specified UUID. + + - generateInvoice(invoice_data: InvoiceData) -> dict: + Generate an invoice based on the provided invoice data. + + """ + + def __init__(self, config: Config) -> None: + """ + Initialize a Bills instance with the provided configuration. + + Args: + config (Config): A configuration object for the Flick API. + """ + self.api = FlickAPI(config=config) + + def onboard_egs(self, egs_data: EGSData): + """ + Onboard an EGS with the provided data. + + Args: + egsData (EGSData): Data representing the EGS to be onboarded. + + Returns: + dict: The response from the API after onboarding the EGS. + + Raises: + Exception: If an error occurs during the onboarding process. + """ + try: + response = self.api.post( + '/egs/onboard', json.dumps(egs_data.to_json())) + return response + except Exception as error: + # Handle errors here + raise error + + def do_compliance_check(self, egs_uuid: str): + """ + Perform a compliance check for an EGS with the specified UUID. + + Args: + egs_uuid (str): The UUID of the EGS for which compliance is checked. + + Returns: + dict: The response from the API after performing the compliance check. + + Raises: + Exception: If an error occurs during the compliance check. + """ + try: + response = self.api.get(f"/egs/compliance-check/{egs_uuid}") + return response + except Exception as error: + # Handle errors here + raise error + + def generate_invoice(self, invoice_data: InvoiceData): + """ + Generate an invoice based on the provided invoice data. + + Args: + invoice_data (InvoiceData): Data for generating the invoice. + + Returns: + dict: The response from the API after generating the invoice. + + Raises: + Exception: If an error occurs during the invoice generation process. + """ + try: + print(json.dumps(invoice_data.to_json())) + response = self.api.post('/invoice/generate', invoice_data.to_json()) + return response + except Exception as error: + # Handle errors here + raise error From bd0755cd8b4e739bb691f947f43110f589cd6692 Mon Sep 17 00:00:00 2001 From: Sivadas Date: Mon, 16 Oct 2023 00:59:06 +0530 Subject: [PATCH 2/3] async done --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ea77c96..99a7e6b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Before using the package, you need to configure it with your API credentials. Yo Here's how you to initiate our SDK in your project: ```python +import asyncio from flick import Config from bills import Bills @@ -44,20 +45,23 @@ The Bills client provides access to various functionalities for managing bills. ```python egs_data = { /* Your EGS data - Check Documentation */ } -response = api.onboard_egs(egsData) +loop = asyncio.get_event_loop() +result = loop.run_until_complete(api.onboard_egs(egs_data=egs_data)) ``` #### Compliance Check: ```python egs_uuid = 'your-egs-uuid'; -response = api.do_compliance_check(egs_uuid); +loop = asyncio.get_event_loop() +result = loop.run_until_complete(api.do_compliance_check(egs_uuid)) ``` #### Generate E-Invoice for Phase-2 in Saudi Arabia: ```python -invoiceData = { /* Your invoice data - Check Documentation */ }; -response = api.generate_invoice(invoiceData); +invoiceData = { /* Your invoice data - Check Documentation */ } +loop = asyncio.get_event_loop() +result = loop.run_until_complete(api.generate_invoice(invoiceData)) ``` ## Examples @@ -65,6 +69,7 @@ response = api.generate_invoice(invoiceData); 1. Here's an Example of how you can **onboard multiple EGS to ZATCA Portal** [If you are onboarding PoS devices or VAT-Group members, this comes handy]. ```python +import asyncio from flick.api_service import Config from flick.bills import Bills,EGSData,Devices @@ -104,14 +109,18 @@ egs_data = EGSData( ] ) -response = client.onboard_egs(egs_data=egs_data) -print(response.text) +loop = asyncio.get_event_loop() +result = loop.run_until_complete(client.onboard_egs(egs_data=egs_data)) +# Process the result the way you want +print(result) + ``` 2. Here's an Example of how you can **Genereate a ZATCA-Complied E-Invoice**. ```python +import asyncio from flick.api_service import Config from flick.bills import Bills, InvoiceData, PartyAddId, PartyDetails, AdvanceDetails, AdvanceInvoices, Invoice, LineItems @@ -179,9 +188,10 @@ invoice_data = InvoiceData( ], ) -response = client.generate_invoice(invoice_data=invoice_data) -print(response.text) - +loop = asyncio.get_event_loop() +result = loop.run_until_complete(client.generate_invoice(invoice_data=invoice_data)) +# Process the result the way you want +print(result) ``` From 4e8a6f3358187d64ae8749862e47194a940bda0c Mon Sep 17 00:00:00 2001 From: Sivadas Date: Mon, 16 Oct 2023 00:59:13 +0530 Subject: [PATCH 3/3] changes --- examples/compiance_check.py | 12 ++++ examples/generate_invoice.py | 7 ++- examples/onboard_egs.py | 9 ++- setup.py | 8 +-- src/flick/api_service.py | 83 ++++++++++----------------- src/flick/bills.py | 106 ++++++++++++++++++++++------------- 6 files changed, 124 insertions(+), 101 deletions(-) create mode 100644 examples/compiance_check.py diff --git a/examples/compiance_check.py b/examples/compiance_check.py new file mode 100644 index 0000000..58bde9d --- /dev/null +++ b/examples/compiance_check.py @@ -0,0 +1,12 @@ +import asyncio +from flick.api_service import Config +from flick.bills import Bills + +config = Config('sandbox', 'your-api-key') + +client = Bills(config) + +loop = asyncio.get_event_loop() +result = loop.run_until_complete(client.do_compliance_check(egs_uuid="your-egs-uuid-here")) +# Process the result the way you want +print(result) \ No newline at end of file diff --git a/examples/generate_invoice.py b/examples/generate_invoice.py index 7a09b6a..9aecc66 100644 --- a/examples/generate_invoice.py +++ b/examples/generate_invoice.py @@ -1,3 +1,4 @@ +import asyncio from flick.api_service import Config from flick.bills import Bills, InvoiceData, PartyAddId, PartyDetails, AdvanceDetails, AdvanceInvoices, Invoice, LineItems @@ -65,5 +66,7 @@ ], ) -response = client.generate_invoice(invoice_data=invoice_data) -print(response.text) +loop = asyncio.get_event_loop() +result = loop.run_until_complete(client.generate_invoice(invoice_data=invoice_data)) +# Process the result the way you want +print(result) \ No newline at end of file diff --git a/examples/onboard_egs.py b/examples/onboard_egs.py index b965408..6ea3b56 100644 --- a/examples/onboard_egs.py +++ b/examples/onboard_egs.py @@ -1,3 +1,4 @@ +import asyncio from flick.api_service import Config from flick.bills import Bills,EGSData,Devices @@ -37,5 +38,9 @@ ] ) -response = client.onboard_egs(egs_data=egs_data) -print(response.text) \ No newline at end of file + +loop = asyncio.get_event_loop() +result = loop.run_until_complete(client.onboard_egs(egs_data=egs_data)) +# Process the result the way you want +print(result) + \ No newline at end of file diff --git a/setup.py b/setup.py index 0b252de..835311e 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,17 @@ -from setuptools import setup,find_packages +from setuptools import setup with open("README.md", "r", encoding = "utf-8") as fh: long_description = fh.read() setup( name = "flick-python-sdk", - version = "0.0.13", + version = "0.0.14", author = "Sivadas Rajan", author_email = "sivadasrajan@gmail.com", description = "A Python wrapper for interacting with APIs from Flick.", long_description = long_description, long_description_content_type = "text/markdown", - install_requires=["requests>=2.25.0"], - url = "https://test.pypi.org/project/flick-python-sdk/", + install_requires=["aiohttp>=3.5.0"], + url = "https://pypi.org/project/flick-python-sdk/", project_urls = { "Bug Tracker": "https://github.com/flick-network/flick-python-sdk/issues", }, diff --git a/src/flick/api_service.py b/src/flick/api_service.py index 22b8fea..e487cab 100644 --- a/src/flick/api_service.py +++ b/src/flick/api_service.py @@ -1,6 +1,6 @@ """ Module for initializing request class """ import urllib -import requests +import aiohttp class InvalidEnvironmentException(Exception): @@ -23,9 +23,17 @@ def __init__(self, environment: str, api_key: str) -> None: "Invalid environment type. use 'sandbox' or 'production'") -class FlickAPI(requests.Session): - """Base class for making all requsts to flick.network""" +class FlickAPI(aiohttp.ClientSession): + """ + A custom aiohttp client session for making authenticated requests to a specified base URL. + Args: + config (Config): The configuration object containing API key and other settings. + + Attributes: + custom_config (Config): The custom configuration object. + base_url (str): The base URL for requests. + """ SANDBOX_BASE_URL = "https://sandbox-api.flick.network" PRODUCTION_BASE_URL = "https://api.flick.network" @@ -38,65 +46,32 @@ def get_base_url(self): return self.SANDBOX_BASE_URL + - def __init__(self, *args, config: Config, **kwargs): + def __init__(self, config: Config): """ - Customised requests.Session class for common base url + Initialize the CustomSession with a custom configuration. Args: - url_base (string, optional): base url for all web requests . Defaults to None. + config (Config): The configuration object containing API key and other settings. """ - self.custom_config = config - super(FlickAPI, self).__init__(*args, **kwargs) - self.url_base = self.get_base_url() - - - - def request(self, method, url, - params=None, - data=None, - headers=None, - cookies=None, - files=None, - auth=None, - timeout=None, - allow_redirects=True, - proxies=None, - hooks=None, - stream=None, - verify=None, - cert=None, - json=None,): - """concatenate and create all relavent urls + super().__init__(headers={'Authorization': f'Bearer {config.api_key}'}) + self.base_url = self.get_base_url() + + def request(self, method, url, **kwargs): + """ + Make a request using the specified method, URL, and additional keyword arguments. Args: - method (str): HTTP Method - url (str): Remaining part of URL + method (str): The HTTP method for the request (e.g., 'GET', 'POST'). + url (str): The path of the URL to request, relative to the base URL. + **kwargs: Additional keyword arguments for the request. Returns: - requests: requests object to use + aiohttp.ClientResponse: The response from the request. """ - modified_url = urllib.parse.urljoin(base=self.url_base, url=url) - - if headers is not None: - headers["Authorization"] = f"Bearer {self.custom_config.api_key}", - else: - headers = { - "Authorization": f"Bearer {self.custom_config.api_key}", - } - - return super(FlickAPI, self).request(method, modified_url, params, - data, - headers, - cookies, - files, - auth, - timeout, - allow_redirects, - proxies, - hooks, - stream, - verify, - cert, - json) + # Prepend the base_url to the request URL + full_url = urllib.parse.urljoin(base=self.base_url, url=url) + return super().request(method, full_url, **kwargs) + \ No newline at end of file diff --git a/src/flick/bills.py b/src/flick/bills.py index ad6d419..e9092f9 100644 --- a/src/flick/bills.py +++ b/src/flick/bills.py @@ -88,24 +88,25 @@ def __init__(self, self.plot_identification = plot_identification self.building = building self.postal_zone = postal_zone + def to_json(self): out = { - "party_name_ar" : self.party_name_ar, - "party_vat" : self.party_vat, - "city_ar" : self.city_ar, - "city_subdivision_ar" : self.city_subdivision_ar, - "street_ar" : self.street_ar, - "postal_zone" : self.postal_zone, - "party_add_id" : self.party_add_id, - "city_en" : self.city_en, - "city_subdivision_en" : self.city_subdivision_en, - "street_en" : self.street_en, - "plot_identification" : self.plot_identification, - "building" : self.building, - "party_name_en" : self.party_name_en, + "party_name_ar": self.party_name_ar, + "party_vat": self.party_vat, + "city_ar": self.city_ar, + "city_subdivision_ar": self.city_subdivision_ar, + "street_ar": self.street_ar, + "postal_zone": self.postal_zone, + "party_add_id": self.party_add_id, + "city_en": self.city_en, + "city_subdivision_en": self.city_subdivision_en, + "street_en": self.street_en, + "plot_identification": self.plot_identification, + "building": self.building, + "party_name_en": self.party_name_en, } - + out["party_add_id"] = self.party_add_id.__dict__ return out @@ -185,8 +186,8 @@ def __init__(self, def to_json(self): out = { - "advance_amount":self.advance_amount, - "total_amount":self.total_amount, + "advance_amount": self.advance_amount, + "total_amount": self.total_amount, } advance_invoices = [] for advance_invoice in self.advance_invoices: @@ -199,7 +200,7 @@ def to_json(self): class InvoiceData: """ The EGSData class. """ - def __init__(self, + def __init__(self, egs_uuid: str, invoice_ref_number: str, issue_date: str, @@ -230,16 +231,16 @@ def __init__(self, def to_json(self): out = { - "egs_uuid" : self.egs_uuid, - "invoice_ref_number" : self.invoice_ref_number, - "issue_date" : self.issue_date, - "issue_time" : self.issue_time, - "doc_type" : self.doc_type, - "inv_type" : self.inv_type, - "payment_method" : self.payment_method, - "has_advance" : self.has_advance, - "currency" : self.currency, - "total_tax" : self.total_tax, + "egs_uuid": self.egs_uuid, + "invoice_ref_number": self.invoice_ref_number, + "issue_date": self.issue_date, + "issue_time": self.issue_time, + "doc_type": self.doc_type, + "inv_type": self.inv_type, + "payment_method": self.payment_method, + "has_advance": self.has_advance, + "currency": self.currency, + "total_tax": self.total_tax, } out["party_details"] = self.party_details.to_json() lineitems = [] @@ -251,6 +252,11 @@ def to_json(self): return out +class NetworkException(Exception): + """ Class for raising network related errors """ + pass + + class Bills: """ A class for handling billing operations using the Flick API. @@ -280,9 +286,10 @@ def __init__(self, config: Config) -> None: Args: config (Config): A configuration object for the Flick API. """ - self.api = FlickAPI(config=config) + self.config = config - def onboard_egs(self, egs_data: EGSData): + + async def onboard_egs(self, egs_data: EGSData): """ Onboard an EGS with the provided data. @@ -295,15 +302,22 @@ def onboard_egs(self, egs_data: EGSData): Raises: Exception: If an error occurs during the onboarding process. """ + try: - response = self.api.post( - '/egs/onboard', json.dumps(egs_data.to_json())) - return response + async with FlickAPI(config=self.config) as session: + async with session.request('POST', '/egs/onboard', data=json.dumps(egs_data.to_json())) as response: + if response.status == 200: + response_text = await response.text() + # Process the response data here + return response_text + else: + raise NetworkException( + f"Request failed with status code: {response.status}") except Exception as error: # Handle errors here raise error - def do_compliance_check(self, egs_uuid: str): + async def do_compliance_check(self, egs_uuid: str): """ Perform a compliance check for an EGS with the specified UUID. @@ -316,14 +330,22 @@ def do_compliance_check(self, egs_uuid: str): Raises: Exception: If an error occurs during the compliance check. """ + try: - response = self.api.get(f"/egs/compliance-check/{egs_uuid}") - return response + async with FlickAPI(config=self.config) as session: + async with session.request('GET', f"/egs/compliance-check/{egs_uuid}") as response: + if response.status == 200: + response_text = await response.text() + # Process the response data here + return response_text + else: + raise NetworkException( + f"Request failed with status code: {response.status}") except Exception as error: # Handle errors here raise error - def generate_invoice(self, invoice_data: InvoiceData): + async def generate_invoice(self, invoice_data: InvoiceData): """ Generate an invoice based on the provided invoice data. @@ -337,9 +359,15 @@ def generate_invoice(self, invoice_data: InvoiceData): Exception: If an error occurs during the invoice generation process. """ try: - print(json.dumps(invoice_data.to_json())) - response = self.api.post('/invoice/generate', invoice_data.to_json()) - return response + async with FlickAPI(config=self.config) as session: + async with session.request('POST', '/invoice/generate', data=json.dumps(invoice_data.to_json())) as response: + if response.status == 200: + response_text = await response.text() + # Process the response data here + return response_text + else: + raise NetworkException( + f"Request failed with status code: {response.status}") except Exception as error: # Handle errors here raise error