From 6ce895a5e185f88be949a252ea232d388637cdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Sat, 25 Jan 2025 19:15:18 +0100 Subject: [PATCH 01/29] Add language and data format enumerations --- superfaktura/enumerations/data_format.py | 6 ++++++ superfaktura/enumerations/language.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 superfaktura/enumerations/data_format.py create mode 100644 superfaktura/enumerations/language.py diff --git a/superfaktura/enumerations/data_format.py b/superfaktura/enumerations/data_format.py new file mode 100644 index 0000000..3ba06b3 --- /dev/null +++ b/superfaktura/enumerations/data_format.py @@ -0,0 +1,6 @@ +import enum + + +class DataFormat(enum.Enum): + JSON = enum.auto() + PDF = enum.auto() diff --git a/superfaktura/enumerations/language.py b/superfaktura/enumerations/language.py new file mode 100644 index 0000000..f3472c8 --- /dev/null +++ b/superfaktura/enumerations/language.py @@ -0,0 +1,15 @@ +class Language: + Czech = "cze" + German = "deu" + English = "eng" + Croatian = "hrv" + Hungarian = "hun" + Italian = "ita" + Dutch = "nld" + Polish = "pol" + Romanian = "rom" + Russian = "rus" + Slovak = "slo" + Slovene = "slv" + Spanish = "spa" + Ukrainian = "ukr" From 4edf8fd84c8d12339baab3989080e1f8b91b813b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Sat, 25 Jan 2025 19:15:42 +0100 Subject: [PATCH 02/29] Add pdf helper -> save temporary file to pdf --- superfaktura/utils/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/superfaktura/utils/__init__.py b/superfaktura/utils/__init__.py index e69de29..4d46350 100644 --- a/superfaktura/utils/__init__.py +++ b/superfaktura/utils/__init__.py @@ -0,0 +1,10 @@ +import tempfile +from typing import IO + + +def save_temporary_file_as_pdf( + temp_file: IO[bytes], output_path: str = "output.pdf" +) -> None: + with open(output_path, "wb") as f: + f.write(temp_file.read()) + temp_file.close() From 5a85c2eb006d4c959ef36bb8ed1e5b3e32b9dc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Sat, 25 Jan 2025 19:17:15 +0100 Subject: [PATCH 03/29] superfaktura_api: allow get request response body in pdf, not only in json --- superfaktura/superfaktura_api.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 9070e42..f979307 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -32,11 +32,14 @@ from typing import Dict import requests from dotenv import load_dotenv # type: ignore +from superfaktura.enumerations.data_format import DataFormat + class SuperFakturaAPIException(Exception): """Exception for errors when working with the SuperFaktura API.""" + class SuperFakturaAPIMissingCredentialsException(Exception): """Exception for missing login credentials.""" @@ -62,7 +65,7 @@ class SuperFakturaAPI: f"{_api_company_id}" } - def get(self, endpoint: str, timeout: int = 5) -> Dict: + def get(self, endpoint: str, data_format: DataFormat = DataFormat.JSON, timeout: int = 5) -> Dict: """ Retrieves data from the SuperFaktura API. @@ -90,10 +93,14 @@ class SuperFakturaAPI: url = f"{self._api_url}/{endpoint}" req = requests.get(url=url, headers=self._auth_header, timeout=timeout) if req.status_code == 200: - return req.json() - raise SuperFakturaAPIException( - f"Get status code: {req.status_code}; {req.json()}" - ) + if data_format == DataFormat.JSON: + return req.json() + elif data_format == DataFormat.PDF: + return {"pdf": req.content} # returns a dict with the PDF content + else: + raise SuperFakturaAPIException( + f"Get status code: {req.status_code}; {req.json()}" + ) def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: """ @@ -130,6 +137,7 @@ class SuperFakturaAPI: ) if req.status_code == 200: return req.json() - raise SuperFakturaAPIException( - f"Post status code: {req.status_code}; {req.json()}" - ) + else: + raise SuperFakturaAPIException( + f"Post status code: {req.status_code}; {req.json()}" + ) From 5d6cb1c3043b6dc018c5414eddedee172f603b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Sat, 25 Jan 2025 19:17:49 +0100 Subject: [PATCH 04/29] invoice: obtain invoice in the pdf format --- superfaktura/invoice.py | 69 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index 4e5ab62..8b1ee3e 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -53,13 +53,16 @@ Usage: """ from dataclasses import dataclass, asdict -from typing import Optional, List +from typing import Optional, List, IO import json from superfaktura.bank_account import BankAccount from superfaktura.client_contacts import ClientContactModel from superfaktura.enumerations.currency import Currencies +from superfaktura.enumerations.data_format import DataFormat +from superfaktura.enumerations.language import Language from superfaktura.superfaktura_api import SuperFakturaAPI +from superfaktura.utils import save_temporary_file_as_pdf from superfaktura.utils.data_types import Date, DateEncoder @@ -147,6 +150,14 @@ class InvoiceItem: return data +@dataclass +class InvoiceRespModel: + error: int + error_message: str + invoice_id: Optional[int] = None + invoice_token: Optional[str] = None + + class InvoiceType: """ " Invoice Type Enumeration. @@ -212,8 +223,18 @@ class Invoice(SuperFakturaAPI): invoice_model: InvoiceModel, items: List[InvoiceItem], contact: ClientContactModel, - ): - """Creates a new invoice.""" + ) -> InvoiceRespModel: + """ + Adds a new invoice. + + Args: + invoice (InvoiceModel): The invoice model. + items (List[InvoiceItem]): List of invoice items. + contact (ClientContactModel): The client contact model. + + Returns: + InvoiceRespModel: The response model for the invoice. + """ data = { "Invoice": invoice_model.as_dict(), "InvoiceItem": [item.as_dict() for item in items], @@ -221,23 +242,50 @@ class Invoice(SuperFakturaAPI): } url = "invoices/create" resp = self.post(endpoint=url, data=json.dumps(data, cls=DateEncoder)) - return resp + invoice_resp = InvoiceRespModel( + error=resp["error"], error_message=resp["error_message"] + ) + if "data" in resp: + if "Invoice" in resp["data"]: + invoice_resp.invoice_id = int(resp["data"]["Invoice"]["id"]) + invoice_resp.invoice_token = resp["data"]["Invoice"]["token"] + return invoice_resp + + def get_pdf( + self, invoice: InvoiceRespModel, language: str = Language.Czech + ) -> IO[bytes]: + """ + Retrieves the PDF of the invoice. + + Args: + invoice (InvoiceRespModel): The response model for the invoice. + language (str): The language for the PDF. + + Returns: + IO[bytes]: A file-like object containing the PDF data. + """ + url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}" + document = self.get(url, data_format=DataFormat.PDF)["pdf"] + temp_pdf = tempfile.TemporaryFile() + temp_pdf.write(document) + temp_pdf.seek(0) + return temp_pdf if __name__ == "__main__": invoice = Invoice() bank = BankAccount() - invoice.add( + resp = invoice.add( invoice_model=InvoiceModel( type=InvoiceType.PROFORMA, - name="Invoice 3", + name="Invoice 5", due=Date("2025-02-01"), invoice_currency=Currencies.CZK, header_comment="We invoice you for services", bank_accounts=[bank.default().as_dict()], ), items=[ - InvoiceItem(name="Services", unit_price=100, quantity=1, unit="ks", tax=21), + InvoiceItem(name="Services", unit_price=100, quantity=5, unit="ks", tax=21), InvoiceItem(name="SIM card", unit_price=50, quantity=1, tax=21, unit="ks"), InvoiceItem( name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" @@ -253,3 +301,10 @@ if __name__ == "__main__": country_id=57, ), ) + _pdf = invoice.get_pdf(resp) + + save_temporary_file_as_pdf(_pdf, "invoice.pdf") + + from pprint import pprint + + pprint(resp) From 84f5cd8642f6975241b6b134fecba76480bce46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Sat, 25 Jan 2025 19:18:07 +0100 Subject: [PATCH 05/29] setup: format --- setup.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 7cf55a7..bdcf4f4 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,12 @@ from setuptools import setup, find_namespace_packages setup( - name='superfaktura_client', - version='0.1.0', + name="superfaktura_client", + version="0.1.0", packages=find_namespace_packages(), - url='https://github.com/eledio-helpers/superfaktura-client', - license='GNU GPLv3', - author='Richard Kubíček', - author_email='kubicekr@eledio.com', - install_requires=[ - "requests", - "python-dotenv" - ], + url="https://github.com/eledio-helpers/superfaktura-client", + license="GNU GPLv3", + author="Richard Kubíček", + author_email="kubicekr@eledio.com", + install_requires=["requests", "python-dotenv"], ) From ff7ab2ba9a4cc057e8b1121db043dd197ea56e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Fri, 28 Feb 2025 18:00:49 +0100 Subject: [PATCH 06/29] fixes after rebase --- docs/source/conf.py | 22 ++++++------- superfaktura/enumerations/data_format.py | 26 ++++++++++++++++ superfaktura/enumerations/language.py | 39 ++++++++++++++++++++++++ superfaktura/invoice.py | 22 ++++++++++--- superfaktura/superfaktura_api.py | 21 +++++++------ 5 files changed, 105 insertions(+), 25 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index dd44d06..9c991a5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,31 +4,31 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath("../..")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'SuperFaktura API client' -copyright = '2025, Richard Kubíček, Eledio s.r.o.' -author = 'Richard Kubíček' +project = "SuperFaktura API client" +copyright = "2025, Richard Kubíček, Eledio s.r.o." +author = "Richard Kubíček" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.autosummary', + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", ] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] diff --git a/superfaktura/enumerations/data_format.py b/superfaktura/enumerations/data_format.py index 3ba06b3..2761512 100644 --- a/superfaktura/enumerations/data_format.py +++ b/superfaktura/enumerations/data_format.py @@ -1,6 +1,32 @@ +""" +Data Format Enumeration. + +This module provides an enumeration of data formats that can be used in the SuperFaktura API. + +Classes: + - DataFormat: Enumeration of data formats. + +Usage: + from superfaktura.enumerations.data_format import DataFormat + data_format = DataFormat.JSON +""" + import enum class DataFormat(enum.Enum): + """ + Data Format Enumeration. + + This enumeration represents the different data formats that can be used in the SuperFaktura API. + + Values: + - JSON: JSON format + - PDF: PDF format + + Usage: + data_format = DataFormat.JSON + """ + JSON = enum.auto() PDF = enum.auto() diff --git a/superfaktura/enumerations/language.py b/superfaktura/enumerations/language.py index f3472c8..34ab924 100644 --- a/superfaktura/enumerations/language.py +++ b/superfaktura/enumerations/language.py @@ -1,4 +1,43 @@ +""" +Language Enumeration. + +This module provides an enumeration of languages that can be used in the SuperFaktura API. + +Classes: + - Language: Enumeration of languages. + +Usage: + from superfaktura.enumerations.language import Language + language = Language.Czech +""" + + class Language: + """ + Language Enumeration. + + This enumeration represents the different languages that can be used in the SuperFaktura API. + + Values: + - Czech: Czech + - German: German + - English: English + - Croatian: Croatian + - Hungarian: Hungarian + - Italian: Italian + - Dutch: Dutch + - Polish: Polish + - Romanian: Romanian + - Russian: Russian + - Slovak: Slovak + - Slovene: Slovene + - Spanish: Spanish + - Ukrainian: Ukrainian + + Usage: + language = Language.Czech + """ + Czech = "cze" German = "deu" English = "eng" diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index 8b1ee3e..e440b11 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -52,6 +52,7 @@ Usage: ) """ +import tempfile from dataclasses import dataclass, asdict from typing import Optional, List, IO import json @@ -152,6 +153,16 @@ class InvoiceItem: @dataclass class InvoiceRespModel: + """ + This dataclass represents the response model for an invoice in the SuperFaktura API. + + Attributes: + - error (int): The error code. + - error_message (str): The error message. + - invoice_id (Optional[int]): The ID of the invoice. + - invoice_token (Optional[str]): The token of + """ + error: int error_message: str invoice_id: Optional[int] = None @@ -159,7 +170,7 @@ class InvoiceRespModel: class InvoiceType: - """ " + """ Invoice Type Enumeration. This enumeration represents the different types of invoices that can be created. @@ -228,12 +239,15 @@ class Invoice(SuperFakturaAPI): Adds a new invoice. Args: - invoice (InvoiceModel): The invoice model. + invoice_model (InvoiceModel): The invoice model. items (List[InvoiceItem]): List of invoice items. contact (ClientContactModel): The client contact model. Returns: InvoiceRespModel: The response model for the invoice. + :param contact: + :param items: + :param invoice_model: """ data = { "Invoice": invoice_model.as_dict(), @@ -278,8 +292,8 @@ if __name__ == "__main__": resp = invoice.add( invoice_model=InvoiceModel( type=InvoiceType.PROFORMA, - name="Invoice 5", - due=Date("2025-02-01"), + name="Invoice 8", + due=Date("2025-04-01"), invoice_currency=Currencies.CZK, header_comment="We invoice you for services", bank_accounts=[bank.default().as_dict()], diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index f979307..cf0e20e 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -39,7 +39,6 @@ class SuperFakturaAPIException(Exception): """Exception for errors when working with the SuperFaktura API.""" - class SuperFakturaAPIMissingCredentialsException(Exception): """Exception for missing login credentials.""" @@ -65,7 +64,9 @@ class SuperFakturaAPI: f"{_api_company_id}" } - def get(self, endpoint: str, data_format: DataFormat = DataFormat.JSON, timeout: int = 5) -> Dict: + def get( + self, endpoint: str, data_format: DataFormat = DataFormat.JSON, timeout: int = 5 + ) -> Dict: """ Retrieves data from the SuperFaktura API. @@ -97,10 +98,11 @@ class SuperFakturaAPI: return req.json() elif data_format == DataFormat.PDF: return {"pdf": req.content} # returns a dict with the PDF content - else: - raise SuperFakturaAPIException( - f"Get status code: {req.status_code}; {req.json()}" - ) + else: + raise SuperFakturaAPIException("Invalid data format") + raise SuperFakturaAPIException( + f"Get status code: {req.status_code}; {req.json()}" + ) def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: """ @@ -137,7 +139,6 @@ class SuperFakturaAPI: ) if req.status_code == 200: return req.json() - else: - raise SuperFakturaAPIException( - f"Post status code: {req.status_code}; {req.json()}" - ) + raise SuperFakturaAPIException( + f"Post status code: {req.status_code}; {req.json()}" + ) From 7b5d787ab8cfe180c68953100772e5aecc847c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Fri, 28 Feb 2025 18:36:05 +0100 Subject: [PATCH 07/29] invoice: do not use tempfile --- superfaktura/invoice.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index e440b11..09e8cb1 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -52,9 +52,8 @@ Usage: ) """ -import tempfile from dataclasses import dataclass, asdict -from typing import Optional, List, IO +from typing import Optional, List import json from superfaktura.bank_account import BankAccount @@ -267,7 +266,7 @@ class Invoice(SuperFakturaAPI): def get_pdf( self, invoice: InvoiceRespModel, language: str = Language.Czech - ) -> IO[bytes]: + ) -> bytes: """ Retrieves the PDF of the invoice. @@ -276,14 +275,11 @@ class Invoice(SuperFakturaAPI): language (str): The language for the PDF. Returns: - IO[bytes]: A file-like object containing the PDF data. + bytes: A bytes containing the PDF data. """ url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}" document = self.get(url, data_format=DataFormat.PDF)["pdf"] - temp_pdf = tempfile.TemporaryFile() - temp_pdf.write(document) - temp_pdf.seek(0) - return temp_pdf + return document if __name__ == "__main__": From e21b7332d0edae7e069ff24aad8021332388617f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Fri, 28 Feb 2025 18:36:24 +0100 Subject: [PATCH 08/29] save_temporary_file_as_pdf: fix method --- superfaktura/utils/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/superfaktura/utils/__init__.py b/superfaktura/utils/__init__.py index 4d46350..48061e1 100644 --- a/superfaktura/utils/__init__.py +++ b/superfaktura/utils/__init__.py @@ -1,10 +1,7 @@ -import tempfile -from typing import IO def save_temporary_file_as_pdf( - temp_file: IO[bytes], output_path: str = "output.pdf" + input_data: bytes, output_path: str = "output.pdf" ) -> None: with open(output_path, "wb") as f: - f.write(temp_file.read()) - temp_file.close() + f.write(input_data) From 1cf1498b2581673c9ab265b448ec5d0d9766e3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Fri, 28 Feb 2025 18:40:18 +0100 Subject: [PATCH 09/29] examples: move save_temporary_file_as_pdf to tools --- examples/tools.py | 7 +++++++ superfaktura/utils/__init__.py | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 examples/tools.py diff --git a/examples/tools.py b/examples/tools.py new file mode 100644 index 0000000..78ceddf --- /dev/null +++ b/examples/tools.py @@ -0,0 +1,7 @@ + + +def save_file_as_pdf( + input_data: bytes, output_path: str = "output.pdf" +) -> None: + with open(output_path, "wb") as f: + f.write(input_data) diff --git a/superfaktura/utils/__init__.py b/superfaktura/utils/__init__.py index 48061e1..e69de29 100644 --- a/superfaktura/utils/__init__.py +++ b/superfaktura/utils/__init__.py @@ -1,7 +0,0 @@ - - -def save_temporary_file_as_pdf( - input_data: bytes, output_path: str = "output.pdf" -) -> None: - with open(output_path, "wb") as f: - f.write(input_data) From fa3d2ddf328e5ff679a0d0da1c1f5103da070165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Fri, 28 Feb 2025 19:11:20 +0100 Subject: [PATCH 10/29] examples: add get_country_list and add_invoice --- examples/add_invoice.py | 72 ++++++++++++++++++++++++++++++++++++ examples/get_country_list.py | 18 +++++++++ examples/tools.py | 13 +++++-- superfaktura/invoice.py | 70 +++++++++++++++-------------------- 4 files changed, 129 insertions(+), 44 deletions(-) create mode 100644 examples/add_invoice.py create mode 100644 examples/get_country_list.py diff --git a/examples/add_invoice.py b/examples/add_invoice.py new file mode 100644 index 0000000..130f618 --- /dev/null +++ b/examples/add_invoice.py @@ -0,0 +1,72 @@ +""" +Main script to add an invoice and save it as a PDF using the SuperFaktura API. + +This script demonstrates how to create an invoice with multiple items, +retrieve the invoice as a PDF, and save the PDF to a file. + +Usage: + Run this script directly to create and save an invoice PDF. + +Dependencies: + - examples.tools.save_file_as_pdf + - superfaktura.bank_account.BankAccount + - superfaktura.client_contacts.ClientContactModel + - superfaktura.enumerations.currency.Currencies + - superfaktura.enumerations.language.Language + - superfaktura.invoice.Invoice + - superfaktura.invoice.InvoiceModel + - superfaktura.invoice.InvoiceType + - superfaktura.invoice.InvoiceItem + - superfaktura.invoice.InvoiceSettings + - superfaktura.utils.data_types.Date +""" + +from examples.tools import save_file_as_pdf +from superfaktura.bank_account import BankAccount +from superfaktura.client_contacts import ClientContactModel +from superfaktura.enumerations.currency import Currencies +from superfaktura.enumerations.language import Language +from superfaktura.invoice import ( + Invoice, + InvoiceModel, + InvoiceType, + InvoiceItem, + InvoiceSettings, +) +from superfaktura.utils.data_types import Date + + +if __name__ == "__main__": + invoice = Invoice() + bank = BankAccount() + resp = invoice.add( + invoice_model=InvoiceModel( + type=InvoiceType.INVOICE, + name="My First Invoice", + due=Date("2025-04-01"), + invoice_currency=Currencies.EUR, + header_comment="We invoice you for services", + bank_accounts=[bank.default().as_dict()], + ), + items=[ + InvoiceItem( + name="Website Development", unit_price=1000.0, quantity=1, tax=20 + ), + InvoiceItem( + name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20 + ), + ], + contact=ClientContactModel( + name="John Doe", + email="john.doe@examle.com", + phone="+1 555-1234", + address="123 Main Street, New York", + ico="987654321", + update=True, + country_id=225, + ), + invoice_settings=InvoiceSettings(language=Language.English), + ) + _pdf = invoice.get_pdf(invoice=resp, language=Language.English) + + save_file_as_pdf(_pdf, "invoice.pdf") diff --git a/examples/get_country_list.py b/examples/get_country_list.py new file mode 100644 index 0000000..208a7c8 --- /dev/null +++ b/examples/get_country_list.py @@ -0,0 +1,18 @@ +""" +This script retrieves and prints the list of countries using the SuperFaktura API. +""" + +from pprint import pprint + +from superfaktura.utils.country import country_list + + +def main(): + """ + Main function to retrieve and print the list of countries using the SuperFaktura API. + """ + pprint(country_list()) + + +if __name__ == "__main__": + main() diff --git a/examples/tools.py b/examples/tools.py index 78ceddf..3a56cc9 100644 --- a/examples/tools.py +++ b/examples/tools.py @@ -1,7 +1,14 @@ +""" +This module contains tools for working with these examples. +""" -def save_file_as_pdf( - input_data: bytes, output_path: str = "output.pdf" -) -> None: +def save_file_as_pdf(input_data: bytes, output_path: str = "output.pdf") -> None: + """ + Save the input data as a PDF file. + :param input_data: + :param output_path: + :return: + """ with open(output_path, "wb") as f: f.write(input_data) diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index 09e8cb1..df85c5e 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -56,13 +56,10 @@ from dataclasses import dataclass, asdict from typing import Optional, List import json -from superfaktura.bank_account import BankAccount from superfaktura.client_contacts import ClientContactModel -from superfaktura.enumerations.currency import Currencies from superfaktura.enumerations.data_format import DataFormat from superfaktura.enumerations.language import Language from superfaktura.superfaktura_api import SuperFakturaAPI -from superfaktura.utils import save_temporary_file_as_pdf from superfaktura.utils.data_types import Date, DateEncoder @@ -168,6 +165,31 @@ class InvoiceRespModel: invoice_token: Optional[str] = None +@dataclass +class InvoiceSettings: + """ + This dataclass represents the settings for an invoice in the SuperFaktura API. + """ + + language: Optional[str] = None + bysquare: Optional[bool] = None + callback_payment: Optional[str] = None + online_payment: Optional[bool] = None + payment_info: Optional[bool] = None + paypal: Optional[bool] = None + show_prices: Optional[bool] = None + signature: Optional[bool] = None + summary_bg_color: Optional[str] = None + + def as_dict(self) -> dict: + """Returns a dictionary representation of the ClientContactModel.""" + data = asdict(self) + for key in list(data.keys()): + if data[key] is None: + del data[key] + return data + + class InvoiceType: """ Invoice Type Enumeration. @@ -233,6 +255,7 @@ class Invoice(SuperFakturaAPI): invoice_model: InvoiceModel, items: List[InvoiceItem], contact: ClientContactModel, + invoice_settings: Optional[InvoiceSettings], ) -> InvoiceRespModel: """ Adds a new invoice. @@ -241,9 +264,11 @@ class Invoice(SuperFakturaAPI): invoice_model (InvoiceModel): The invoice model. items (List[InvoiceItem]): List of invoice items. contact (ClientContactModel): The client contact model. + invoice_settings (Optional[InvoiceSettings]): The invoice settings. Returns: InvoiceRespModel: The response model for the invoice. + :param invoice_settings: :param contact: :param items: :param invoice_model: @@ -252,6 +277,7 @@ class Invoice(SuperFakturaAPI): "Invoice": invoice_model.as_dict(), "InvoiceItem": [item.as_dict() for item in items], "Client": contact.as_dict(), + "InvoiceSetting": invoice_settings.as_dict(), } url = "invoices/create" resp = self.post(endpoint=url, data=json.dumps(data, cls=DateEncoder)) @@ -280,41 +306,3 @@ class Invoice(SuperFakturaAPI): url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}" document = self.get(url, data_format=DataFormat.PDF)["pdf"] return document - - -if __name__ == "__main__": - invoice = Invoice() - bank = BankAccount() - resp = invoice.add( - invoice_model=InvoiceModel( - type=InvoiceType.PROFORMA, - name="Invoice 8", - due=Date("2025-04-01"), - invoice_currency=Currencies.CZK, - header_comment="We invoice you for services", - bank_accounts=[bank.default().as_dict()], - ), - items=[ - InvoiceItem(name="Services", unit_price=100, quantity=5, unit="ks", tax=21), - InvoiceItem(name="SIM card", unit_price=50, quantity=1, tax=21, unit="ks"), - InvoiceItem( - name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" - ), - ], - contact=ClientContactModel( - name="Richard Kubíček", - email="kubicekr@eledio.com", - phone="+420 123 456 789", - address="Jaroslava Foglara 861/1", - ico="123", - update=True, - country_id=57, - ), - ) - _pdf = invoice.get_pdf(resp) - - save_temporary_file_as_pdf(_pdf, "invoice.pdf") - - from pprint import pprint - - pprint(resp) From a20ae3fad04a9b95f82be13c51b54c567565bfa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Fri, 28 Feb 2025 19:17:41 +0100 Subject: [PATCH 11/29] examples: add examples to the docs --- docs/source/examples.rst | 19 +++++++++++++++++++ docs/source/index.rst | 1 + examples/add_invoice.py | 9 ++++++++- examples/tools.py | 6 ++++-- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 docs/source/examples.rst diff --git a/docs/source/examples.rst b/docs/source/examples.rst new file mode 100644 index 0000000..d0c5e94 --- /dev/null +++ b/docs/source/examples.rst @@ -0,0 +1,19 @@ +Examples of SuperFaktura API client usage +==================== + + +Add Invoice +--------------------------------- + +.. automodule:: examples.add_invoice + :members: + :undoc-members: + :show-inheritance: + +Get country list +------------------------------------ + +.. automodule:: examples.get_country_list + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/index.rst b/docs/source/index.rst index f27d38e..4f2a10f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,3 +6,4 @@ Welcome to SuperFaktura Client's documentation superfaktura installation + examples diff --git a/examples/add_invoice.py b/examples/add_invoice.py index 130f618..9184a9b 100644 --- a/examples/add_invoice.py +++ b/examples/add_invoice.py @@ -36,7 +36,10 @@ from superfaktura.invoice import ( from superfaktura.utils.data_types import Date -if __name__ == "__main__": +def main(): + """ + Main function to add Invoice and save it as a pdf using the SuperFaktura API. + """ invoice = Invoice() bank = BankAccount() resp = invoice.add( @@ -70,3 +73,7 @@ if __name__ == "__main__": _pdf = invoice.get_pdf(invoice=resp, language=Language.English) save_file_as_pdf(_pdf, "invoice.pdf") + + +if __name__ == "__main__": + main() diff --git a/examples/tools.py b/examples/tools.py index 3a56cc9..840552e 100644 --- a/examples/tools.py +++ b/examples/tools.py @@ -2,6 +2,8 @@ This module contains tools for working with these examples. """ +from pathlib import Path + def save_file_as_pdf(input_data: bytes, output_path: str = "output.pdf") -> None: """ @@ -10,5 +12,5 @@ def save_file_as_pdf(input_data: bytes, output_path: str = "output.pdf") -> None :param output_path: :return: """ - with open(output_path, "wb") as f: - f.write(input_data) + p = Path(output_path) + p.write_bytes(input_data) From 92ae1f615e912fff87bb70efb4c9a8005b67ed67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Mon, 3 Mar 2025 19:43:58 +0100 Subject: [PATCH 12/29] examples: simplify add_invoice --- examples/add_invoice.py | 6 ++++-- examples/tools.py | 16 ---------------- 2 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 examples/tools.py diff --git a/examples/add_invoice.py b/examples/add_invoice.py index 9184a9b..e085ad0 100644 --- a/examples/add_invoice.py +++ b/examples/add_invoice.py @@ -21,7 +21,8 @@ Dependencies: - superfaktura.utils.data_types.Date """ -from examples.tools import save_file_as_pdf +from pathlib import Path + from superfaktura.bank_account import BankAccount from superfaktura.client_contacts import ClientContactModel from superfaktura.enumerations.currency import Currencies @@ -72,7 +73,8 @@ def main(): ) _pdf = invoice.get_pdf(invoice=resp, language=Language.English) - save_file_as_pdf(_pdf, "invoice.pdf") + p = Path("invoice.pdf") + p.write_bytes(_pdf) if __name__ == "__main__": diff --git a/examples/tools.py b/examples/tools.py deleted file mode 100644 index 840552e..0000000 --- a/examples/tools.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This module contains tools for working with these examples. -""" - -from pathlib import Path - - -def save_file_as_pdf(input_data: bytes, output_path: str = "output.pdf") -> None: - """ - Save the input data as a PDF file. - :param input_data: - :param output_path: - :return: - """ - p = Path(output_path) - p.write_bytes(input_data) From 73da343c610420f68792306d5fcdd46f1c86edb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Mon, 3 Mar 2025 20:09:52 +0100 Subject: [PATCH 13/29] api: remove data_format and simplify usages of get request --- superfaktura/bank_account.py | 4 ++- superfaktura/client_contacts.py | 8 +++--- superfaktura/enumerations/data_format.py | 32 ------------------------ superfaktura/invoice.py | 7 +++--- superfaktura/superfaktura_api.py | 17 +++---------- superfaktura/utils/country.py | 5 +++- 6 files changed, 19 insertions(+), 54 deletions(-) delete mode 100644 superfaktura/enumerations/data_format.py diff --git a/superfaktura/bank_account.py b/superfaktura/bank_account.py index 748ba85..fb188f6 100644 --- a/superfaktura/bank_account.py +++ b/superfaktura/bank_account.py @@ -31,6 +31,7 @@ Usage: bank.post(data) """ +import json from dataclasses import dataclass, asdict from typing import Optional @@ -96,7 +97,8 @@ class BankAccount(SuperFakturaAPI): def list(self) -> dict: """Retrieves a list of bank accounts.""" url = "bank_accounts/index" - return self.get(url) + bank_accounts = self.get(url) + return json.loads(bank_accounts) def default(self) -> Optional[BankAccountModel]: """Retrieves the default bank account.""" diff --git a/superfaktura/client_contacts.py b/superfaktura/client_contacts.py index a6a0c4b..70676c8 100644 --- a/superfaktura/client_contacts.py +++ b/superfaktura/client_contacts.py @@ -104,12 +104,14 @@ class ClientContact(SuperFakturaAPI): def list(self) -> dict: """Lists all exists client contacts.""" url = "clients/index.json" - return self.get(endpoint=url) + clients = self.get(endpoint=url) + return json.loads(clients) def get_client(self, client_id: int) -> ClientContactModel: """Gets a client contact by ID.""" url = f"clients/view/{client_id}" - data = self.get(endpoint=url) + clients = self.get(endpoint=url) + data = json.loads(clients) if "Client" not in data: raise ClientException("Client not found") data = data["Client"] @@ -122,4 +124,4 @@ if __name__ == "__main__": pprint(resp) - pprint(client.get_client(40019)) + pprint(client.get_client(40011)) diff --git a/superfaktura/enumerations/data_format.py b/superfaktura/enumerations/data_format.py deleted file mode 100644 index 2761512..0000000 --- a/superfaktura/enumerations/data_format.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Data Format Enumeration. - -This module provides an enumeration of data formats that can be used in the SuperFaktura API. - -Classes: - - DataFormat: Enumeration of data formats. - -Usage: - from superfaktura.enumerations.data_format import DataFormat - data_format = DataFormat.JSON -""" - -import enum - - -class DataFormat(enum.Enum): - """ - Data Format Enumeration. - - This enumeration represents the different data formats that can be used in the SuperFaktura API. - - Values: - - JSON: JSON format - - PDF: PDF format - - Usage: - data_format = DataFormat.JSON - """ - - JSON = enum.auto() - PDF = enum.auto() diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index df85c5e..56d4c79 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -57,7 +57,6 @@ from typing import Optional, List import json from superfaktura.client_contacts import ClientContactModel -from superfaktura.enumerations.data_format import DataFormat from superfaktura.enumerations.language import Language from superfaktura.superfaktura_api import SuperFakturaAPI from superfaktura.utils.data_types import Date, DateEncoder @@ -255,7 +254,7 @@ class Invoice(SuperFakturaAPI): invoice_model: InvoiceModel, items: List[InvoiceItem], contact: ClientContactModel, - invoice_settings: Optional[InvoiceSettings], + invoice_settings: Optional[InvoiceSettings] = None, ) -> InvoiceRespModel: """ Adds a new invoice. @@ -277,7 +276,7 @@ class Invoice(SuperFakturaAPI): "Invoice": invoice_model.as_dict(), "InvoiceItem": [item.as_dict() for item in items], "Client": contact.as_dict(), - "InvoiceSetting": invoice_settings.as_dict(), + "InvoiceSetting": invoice_settings.as_dict() if invoice_settings else {}, } url = "invoices/create" resp = self.post(endpoint=url, data=json.dumps(data, cls=DateEncoder)) @@ -304,5 +303,5 @@ class Invoice(SuperFakturaAPI): bytes: A bytes containing the PDF data. """ url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}" - document = self.get(url, data_format=DataFormat.PDF)["pdf"] + document = self.get(url) return document diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index cf0e20e..40636b0 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -32,8 +32,6 @@ from typing import Dict import requests from dotenv import load_dotenv # type: ignore -from superfaktura.enumerations.data_format import DataFormat - class SuperFakturaAPIException(Exception): """Exception for errors when working with the SuperFaktura API.""" @@ -64,9 +62,7 @@ class SuperFakturaAPI: f"{_api_company_id}" } - def get( - self, endpoint: str, data_format: DataFormat = DataFormat.JSON, timeout: int = 5 - ) -> Dict: + def get(self, endpoint: str, timeout: int = 5) -> bytes: """ Retrieves data from the SuperFaktura API. @@ -78,7 +74,7 @@ class SuperFakturaAPI: timeout (int, optional): The timeout for the API request in seconds. Defaults to 5. Returns: - Dict: The retrieved data in JSON format. + bytes: The retrieved data in bytes. Raises: SuperFakturaAPIException: If the API request fails or returns an error. @@ -94,14 +90,9 @@ class SuperFakturaAPI: url = f"{self._api_url}/{endpoint}" req = requests.get(url=url, headers=self._auth_header, timeout=timeout) if req.status_code == 200: - if data_format == DataFormat.JSON: - return req.json() - elif data_format == DataFormat.PDF: - return {"pdf": req.content} # returns a dict with the PDF content - else: - raise SuperFakturaAPIException("Invalid data format") + return req.content raise SuperFakturaAPIException( - f"Get status code: {req.status_code}; {req.json()}" + f"Get status code: {req.status_code}; {req.content!r}" ) def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: diff --git a/superfaktura/utils/country.py b/superfaktura/utils/country.py index 87b2672..557a3d2 100644 --- a/superfaktura/utils/country.py +++ b/superfaktura/utils/country.py @@ -12,6 +12,8 @@ Usage: print(countries) """ +import json + from superfaktura.superfaktura_api import SuperFakturaAPI @@ -30,4 +32,5 @@ def country_list(): """ api = SuperFakturaAPI() url = "countries" - return api.get(url) + countries = api.get(url) + return json.loads(countries) From ec9a83be30e2b147ff26863a8967c923f748a74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Mon, 3 Mar 2025 20:13:40 +0100 Subject: [PATCH 14/29] setup: fix licence --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bdcf4f4..bc516cd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( version="0.1.0", packages=find_namespace_packages(), url="https://github.com/eledio-helpers/superfaktura-client", - license="GNU GPLv3", + license="MIT", author="Richard Kubíček", author_email="kubicekr@eledio.com", install_requires=["requests", "python-dotenv"], From a49aba6d46d6b394a03808d9d1035d2d27a344e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Mon, 3 Mar 2025 20:37:07 +0100 Subject: [PATCH 15/29] superfaktura_api: update usages in docs --- superfaktura/superfaktura_api.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 40636b0..5de564f 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -14,16 +14,13 @@ Functions: - post: Creates or updates data in the SuperFaktura API. Usage: - import superfaktura.superfaktura_api - - # Create an instance of SuperFakturaAPI - api = superfaktura.superfaktura_api.SuperFakturaAPI() - - # Retrieve data from the SuperFaktura API - data = api.get('endpoint') - - # Create or update data in the SuperFaktura API - api.post('endpoint', data) + >>> import superfaktura.superfaktura_api + >>> # Create an instance of SuperFakturaAPI + >>> api = superfaktura.superfaktura_api.SuperFakturaAPI() + >>> # Retrieve data from the SuperFaktura API + >>> incoming_data = api.get('endpoint') + >>> # Create or update data in the SuperFaktura API + >>> api.post('endpoint', outgoing_data) """ import os From 89536bcb83d975cbee1389d107abcf0577f5fa1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Mon, 3 Mar 2025 20:44:33 +0100 Subject: [PATCH 16/29] invoice: update usages in docs --- superfaktura/invoice.py | 80 ++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index 56d4c79..da1caea 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -16,38 +16,35 @@ Functions: - (none) Usage: - import superfaktura.invoice - - # Create an instance of Invoice - invoice = superfaktura.invoice.Invoice() - - # Create an invoice - invoice.add( - invoice=superfaktura.invoice.InvoiceModel( - type=superfaktura.invoice.InvoiceType.PROFORMA, - name="Invoice 3", - due=superfaktura.invoice.Date("2025-02-01"), - invoice_currency=superfaktura.invoice.Currencies.CZK, + >>> import superfaktura.invoice + >>> # Create an instance of Invoice + >>> invoice = superfaktura.invoice.Invoice() + >>> # Create an invoice + >>> invoice.add( + invoice_model=InvoiceModel( + type=InvoiceType.INVOICE, + name="My First Invoice", + due=Date("2025-04-01"), + invoice_currency=Currencies.EUR, header_comment="We invoice you for services", bank_accounts=[bank.default().as_dict()], ), items=[ - superfaktura.invoice.InvoiceItem(name="Services", unit_price=100, quantity=1, - unit="ks", tax=21), - superfaktura.invoice.InvoiceItem(name="SIM card", unit_price=50, quantity=1, - tax=21, unit="ks"), - superfaktura.invoice.InvoiceItem( - name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" + InvoiceItem( + name="Website Development", unit_price=1000.0, quantity=1, tax=20 + ), + InvoiceItem( + name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20 ), ], - contact=superfaktura.client_contacts.ClientContactModel( - name="Richard Kubíček", - email="kubicekr@eledio.com", - phone="+420 123 456 789", - address="Jaroslava Foglara 861/1", - ico="123", + contact=ClientContactModel( + name="John Doe", + email="john.doe@examle.com", + phone="+1 555-1234", + address="123 Main Street, New York", + ico="987654321", update=True, - country_id=57, + country_id=225, ), ) """ @@ -217,31 +214,32 @@ class Invoice(SuperFakturaAPI): - update: Updates an existing invoice. Usage: - invoice = Invoice() - invoice.add( - invoice=InvoiceModel( - type=InvoiceType.PROFORMA, - name="Invoice 3", - due=Date("2025-02-01"), - invoice_currency=Currencies.CZK, + >>> invoice = Invoice() + >>> invoice.add( + invoice_model=InvoiceModel( + type=InvoiceType.INVOICE, + name="My First Invoice", + due=Date("2025-04-01"), + invoice_currency=Currencies.EUR, header_comment="We invoice you for services", bank_accounts=[bank.default().as_dict()], ), items=[ - InvoiceItem(name="Services", unit_price=100, quantity=1, unit="ks", tax=21), - InvoiceItem(name="SIM card", unit_price=50, quantity=1, tax=21, unit="ks"), InvoiceItem( - name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" + name="Website Development", unit_price=1000.0, quantity=1, tax=20 + ), + InvoiceItem( + name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20 ), ], contact=ClientContactModel( - name="Richard Kubíček", - email="kubicekr@eledio.com", - phone="+420 123 456 789", - address="Jaroslava Foglara 861/1", - ico="123", + name="John Doe", + email="john.doe@examle.com", + phone="+1 555-1234", + address="123 Main Street, New York", + ico="987654321", update=True, - country_id=57, + country_id=225, ), ) """ From 30f0e36643ce1592f85ad346107a8506ba23cad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Mon, 3 Mar 2025 20:45:42 +0100 Subject: [PATCH 17/29] bank_account: update usages in docs --- superfaktura/bank_account.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/superfaktura/bank_account.py b/superfaktura/bank_account.py index fb188f6..bba7633 100644 --- a/superfaktura/bank_account.py +++ b/superfaktura/bank_account.py @@ -15,20 +15,16 @@ Functions: - (none) Usage: - import superfaktura.bank_account - - # Create an instance of BankAccount - bank = superfaktura.bank_account.BankAccount() - - # Retrieve a list of bank accounts - accounts = bank.list() - - # Get the default bank account - default_account = bank.default() - - # Create or update a bank account - data = {"account": "1234567890", "bank_code": "1234567890", "default": True} - bank.post(data) + >>> import superfaktura.bank_account + >>> # Create an instance of BankAccount + >>> bank = superfaktura.bank_account.BankAccount() + >>> # Retrieve a list of bank accounts + >>> accounts = bank.list() + >>> # Get the default bank account + >>> default_account = bank.default() + >>> # Create or update a bank account + >>> data = {"account": "1234567890", "bank_code": "1234567890", "default": True} + >>> bank.post(data) """ import json @@ -84,11 +80,11 @@ class BankAccount(SuperFakturaAPI): - post: Creates or updates a bank account. Usage: - bank = BankAccount() - accounts = bank.list() - default_account = bank.default() - data = {"account": "1234567890", "bank_code": "1234567890", "default": True} - bank.post(data) + >>> bank = BankAccount() + >>> accounts = bank.list() + >>> default_account = bank.default() + >>> data = {"account": "1234567890", "bank_code": "1234567890", "default": True} + >>> bank.post(data) """ def __init__(self): From 532c58cbd5f262597ec8af8510bfeb71a8332acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:06:16 +0100 Subject: [PATCH 18/29] api: simplify api get request --- superfaktura/superfaktura_api.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 5de564f..a384674 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -59,7 +59,7 @@ class SuperFakturaAPI: f"{_api_company_id}" } - def get(self, endpoint: str, timeout: int = 5) -> bytes: + def get(self, endpoint: str, timeout: int = 5) -> Dict: """ Retrieves data from the SuperFaktura API. @@ -71,7 +71,7 @@ class SuperFakturaAPI: timeout (int, optional): The timeout for the API request in seconds. Defaults to 5. Returns: - bytes: The retrieved data in bytes. + Dict: The retrieved data in JSON format. Raises: SuperFakturaAPIException: If the API request fails or returns an error. @@ -87,10 +87,12 @@ class SuperFakturaAPI: url = f"{self._api_url}/{endpoint}" req = requests.get(url=url, headers=self._auth_header, timeout=timeout) if req.status_code == 200: - return req.content - raise SuperFakturaAPIException( - f"Get status code: {req.status_code}; {req.content!r}" - ) + try: + return req.json() + except requests.exceptions.JSONDecodeError as e: + raise SuperFakturaAPIException( + f"Get status code: {req.status_code}; {req.content}; {e}" + ) def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: """ From 2eedefec8363be32b52591f92ab4d46fa8094eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:05:41 +0100 Subject: [PATCH 19/29] Revert "Auxiliary commit to revert individual files from 73da343c610420f68792306d5fcdd46f1c86edb8" This reverts commit e925cf182acf7c18cfd27351e72baed328d43e2c. --- superfaktura/bank_account.py | 4 +--- superfaktura/client_contacts.py | 8 +++----- superfaktura/utils/country.py | 5 +---- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/superfaktura/bank_account.py b/superfaktura/bank_account.py index bba7633..32546a5 100644 --- a/superfaktura/bank_account.py +++ b/superfaktura/bank_account.py @@ -27,7 +27,6 @@ Usage: >>> bank.post(data) """ -import json from dataclasses import dataclass, asdict from typing import Optional @@ -93,8 +92,7 @@ class BankAccount(SuperFakturaAPI): def list(self) -> dict: """Retrieves a list of bank accounts.""" url = "bank_accounts/index" - bank_accounts = self.get(url) - return json.loads(bank_accounts) + return self.get(url) def default(self) -> Optional[BankAccountModel]: """Retrieves the default bank account.""" diff --git a/superfaktura/client_contacts.py b/superfaktura/client_contacts.py index 70676c8..a6a0c4b 100644 --- a/superfaktura/client_contacts.py +++ b/superfaktura/client_contacts.py @@ -104,14 +104,12 @@ class ClientContact(SuperFakturaAPI): def list(self) -> dict: """Lists all exists client contacts.""" url = "clients/index.json" - clients = self.get(endpoint=url) - return json.loads(clients) + return self.get(endpoint=url) def get_client(self, client_id: int) -> ClientContactModel: """Gets a client contact by ID.""" url = f"clients/view/{client_id}" - clients = self.get(endpoint=url) - data = json.loads(clients) + data = self.get(endpoint=url) if "Client" not in data: raise ClientException("Client not found") data = data["Client"] @@ -124,4 +122,4 @@ if __name__ == "__main__": pprint(resp) - pprint(client.get_client(40011)) + pprint(client.get_client(40019)) diff --git a/superfaktura/utils/country.py b/superfaktura/utils/country.py index 557a3d2..87b2672 100644 --- a/superfaktura/utils/country.py +++ b/superfaktura/utils/country.py @@ -12,8 +12,6 @@ Usage: print(countries) """ -import json - from superfaktura.superfaktura_api import SuperFakturaAPI @@ -32,5 +30,4 @@ def country_list(): """ api = SuperFakturaAPI() url = "countries" - countries = api.get(url) - return json.loads(countries) + return api.get(url) From bb6b6343b0905eacda5d7bc5decad318d74e4283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:10:34 +0100 Subject: [PATCH 20/29] superfaktura_api: fix return values of get method --- superfaktura/superfaktura_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index a384674..65880d5 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -91,8 +91,11 @@ class SuperFakturaAPI: return req.json() except requests.exceptions.JSONDecodeError as e: raise SuperFakturaAPIException( - f"Get status code: {req.status_code}; {req.content}; {e}" + f"Unable to decode response as JSON; {req.content!r}; {e}" ) + raise SuperFakturaAPIException( + f"Get status code: {req.status_code}; {req.content!r}" + ) def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: """ From 8d5bf14be3a41007970ef6c61c67d579ef5d982f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:40:40 +0100 Subject: [PATCH 21/29] superfaktura_api: add download method for save IO[bytes] data stream --- examples/add_invoice.py | 7 ++--- superfaktura/invoice.py | 15 ++++++----- superfaktura/superfaktura_api.py | 44 +++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/examples/add_invoice.py b/examples/add_invoice.py index e085ad0..400f27f 100644 --- a/examples/add_invoice.py +++ b/examples/add_invoice.py @@ -21,8 +21,6 @@ Dependencies: - superfaktura.utils.data_types.Date """ -from pathlib import Path - from superfaktura.bank_account import BankAccount from superfaktura.client_contacts import ClientContactModel from superfaktura.enumerations.currency import Currencies @@ -71,10 +69,9 @@ def main(): ), invoice_settings=InvoiceSettings(language=Language.English), ) - _pdf = invoice.get_pdf(invoice=resp, language=Language.English) - p = Path("invoice.pdf") - p.write_bytes(_pdf) + with open("invoice.pdf", "wb") as f: + invoice.get_pdf(invoice=resp, descriptor=f, language=Language.English) if __name__ == "__main__": diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index da1caea..cbbd827 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -50,7 +50,7 @@ Usage: """ from dataclasses import dataclass, asdict -from typing import Optional, List +from typing import Optional, List, IO import json from superfaktura.client_contacts import ClientContactModel @@ -288,18 +288,21 @@ class Invoice(SuperFakturaAPI): return invoice_resp def get_pdf( - self, invoice: InvoiceRespModel, language: str = Language.Czech - ) -> bytes: + self, + invoice: InvoiceRespModel, + descriptor: IO[bytes], + language: str = Language.Czech, + ) -> None: """ Retrieves the PDF of the invoice. Args: invoice (InvoiceRespModel): The response model for the invoice. + descriptor (IO[bytes]): The descriptor to write the PDF data to. language (str): The language for the PDF. Returns: - bytes: A bytes containing the PDF data. + None """ url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}" - document = self.get(url) - return document + self.download(url, descriptor) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 65880d5..9e6e559 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -24,7 +24,7 @@ Usage: """ import os -from typing import Dict +from typing import Dict, IO import requests from dotenv import load_dotenv # type: ignore @@ -97,6 +97,48 @@ class SuperFakturaAPI: f"Get status code: {req.status_code}; {req.content!r}" ) + def download(self, endpoint: str, descriptor: IO[bytes], timeout: int = 5) -> None: + """ + Download data stream from the SuperFaktura API. + + Download data stream from the specified endpoint in the SuperFaktura API. + + Args: + endpoint (str): The API endpoint to retrieve data from (e.g. 'invoices', 'clients', + etc.). + descriptor (IO[bytes]): The descriptor to write the data stream to. + timeout (int, optional): The timeout for the API request in seconds. Defaults to 5. + + Returns: + None + + Raises: + SuperFakturaAPIException: If the API request fails or returns an error. + + Examples: + >>> from superfaktura.invoice import Invoice + >>> from superfaktura.enumerations.language import Language + >>> invoice = Invoice() + >>> with open("invoice.pdf", "wb") as f: + >>> invoice.get_pdf(invoice=invoice_data, descriptor=f, language=Language.English) + + Notes: + The available endpoints can be found in the SuperFaktura API documentation. + """ + url = f"{self._api_url}/{endpoint}" + req = requests.get(url=url, headers=self._auth_header, timeout=timeout) + if req.status_code == 200: + if descriptor.writable(): + descriptor.write(req.content) + else: + raise SuperFakturaAPIException( + f"Descriptor {descriptor} is not writable" + ) + else: + raise SuperFakturaAPIException( + f"Download status code: {req.status_code}; {req.content!r}" + ) + def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: """ Creates or updates data in the SuperFaktura API. From 3d39ac39b26d7dbe598c245bef4df22adb52882c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:47:07 +0100 Subject: [PATCH 22/29] superfaktura_api: preserve the exception context --- superfaktura/superfaktura_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 9e6e559..9f53bb2 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -92,7 +92,7 @@ class SuperFakturaAPI: except requests.exceptions.JSONDecodeError as e: raise SuperFakturaAPIException( f"Unable to decode response as JSON; {req.content!r}; {e}" - ) + ) from e raise SuperFakturaAPIException( f"Get status code: {req.status_code}; {req.content!r}" ) From e8a379fc4f77eb87a6163ba1eeb0a2c44a07478d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:47:52 +0100 Subject: [PATCH 23/29] superfaktura_api: fix download method docstring example --- superfaktura/superfaktura_api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 9f53bb2..05d3adc 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -116,11 +116,9 @@ class SuperFakturaAPI: SuperFakturaAPIException: If the API request fails or returns an error. Examples: - >>> from superfaktura.invoice import Invoice - >>> from superfaktura.enumerations.language import Language - >>> invoice = Invoice() - >>> with open("invoice.pdf", "wb") as f: - >>> invoice.get_pdf(invoice=invoice_data, descriptor=f, language=Language.English) + >>> api = SuperFakturaAPI() + >>> with open("data.pdf", "wb") as f: + >>> api.download("some/endpoint", descriptor=f) Notes: The available endpoints can be found in the SuperFaktura API documentation. From 735f212a22a809fecf3a7c8c50a70d4cd7268d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:49:14 +0100 Subject: [PATCH 24/29] invoice: fix docstring --- superfaktura/invoice.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index cbbd827..4f43648 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -152,7 +152,7 @@ class InvoiceRespModel: - error (int): The error code. - error_message (str): The error message. - invoice_id (Optional[int]): The ID of the invoice. - - invoice_token (Optional[str]): The token of + - invoice_token (Optional[str]): The token of the invoice. """ error: int @@ -178,7 +178,7 @@ class InvoiceSettings: summary_bg_color: Optional[str] = None def as_dict(self) -> dict: - """Returns a dictionary representation of the ClientContactModel.""" + """Returns a dictionary representation of the InvoiceSettings.""" data = asdict(self) for key in list(data.keys()): if data[key] is None: @@ -265,10 +265,6 @@ class Invoice(SuperFakturaAPI): Returns: InvoiceRespModel: The response model for the invoice. - :param invoice_settings: - :param contact: - :param items: - :param invoice_model: """ data = { "Invoice": invoice_model.as_dict(), From 0319228ba1013a3158e6a59522c6eb3802ed5015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:52:44 +0100 Subject: [PATCH 25/29] ruff: add configuration --- ruff.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ruff.toml diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d02b9fd --- /dev/null +++ b/ruff.toml @@ -0,0 +1,5 @@ +[lint] +ignore = [ + # Use a single `if` statement instead of nested `if` statements + "SIM102", +] \ No newline at end of file From 4a66302303531e1d90fea90866a8acd5ffdc93c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:59:01 +0100 Subject: [PATCH 26/29] add_inovice: add some protection clauses --- examples/add_invoice.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/add_invoice.py b/examples/add_invoice.py index 400f27f..d292a5c 100644 --- a/examples/add_invoice.py +++ b/examples/add_invoice.py @@ -21,7 +21,7 @@ Dependencies: - superfaktura.utils.data_types.Date """ -from superfaktura.bank_account import BankAccount +from superfaktura.bank_account import BankAccount, NoDefaultBankAccountException from superfaktura.client_contacts import ClientContactModel from superfaktura.enumerations.currency import Currencies from superfaktura.enumerations.language import Language @@ -41,6 +41,12 @@ def main(): """ invoice = Invoice() bank = BankAccount() + try: + # Get default bank account + bank_account = bank.default().as_dict() + except NoDefaultBankAccountException as e: + print(f"Error getting default bank account: {e}") + bank_account = {} resp = invoice.add( invoice_model=InvoiceModel( type=InvoiceType.INVOICE, @@ -48,7 +54,7 @@ def main(): due=Date("2025-04-01"), invoice_currency=Currencies.EUR, header_comment="We invoice you for services", - bank_accounts=[bank.default().as_dict()], + bank_accounts=[bank_account], ), items=[ InvoiceItem( From e43bf6eb82954a40a14d57b676e4e869ba0c555e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 13:59:41 +0100 Subject: [PATCH 27/29] invoice: fix typo --- superfaktura/invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index 4f43648..452f7f2 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -234,7 +234,7 @@ class Invoice(SuperFakturaAPI): ], contact=ClientContactModel( name="John Doe", - email="john.doe@examle.com", + email="john.doe@example.com", phone="+1 555-1234", address="123 Main Street, New York", ico="987654321", From b1b887da10b8d7e08f8b7f181011200fb3243d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 14:04:37 +0100 Subject: [PATCH 28/29] add_invoice: remove unused dependency --- examples/add_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/add_invoice.py b/examples/add_invoice.py index d292a5c..d9ff064 100644 --- a/examples/add_invoice.py +++ b/examples/add_invoice.py @@ -8,7 +8,6 @@ Usage: Run this script directly to create and save an invoice PDF. Dependencies: - - examples.tools.save_file_as_pdf - superfaktura.bank_account.BankAccount - superfaktura.client_contacts.ClientContactModel - superfaktura.enumerations.currency.Currencies From d837a2e38549f316244e5acdd3a239755047db91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Tue, 4 Mar 2025 14:07:07 +0100 Subject: [PATCH 29/29] add_invoice: add try-except clause --- examples/add_invoice.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/add_invoice.py b/examples/add_invoice.py index d9ff064..8fb8e9a 100644 --- a/examples/add_invoice.py +++ b/examples/add_invoice.py @@ -75,8 +75,12 @@ def main(): invoice_settings=InvoiceSettings(language=Language.English), ) - with open("invoice.pdf", "wb") as f: - invoice.get_pdf(invoice=resp, descriptor=f, language=Language.English) + try: + with open("invoice.pdf", "wb") as f: + invoice.get_pdf(invoice=resp, descriptor=f, language=Language.English) + print("Invoice saved to 'invoice.pdf'") + except Exception as e: + print(f"Error generating or saving invoice as PDF: {e}") if __name__ == "__main__":