From c56243589b652dd8969ab2b362268d7ed9a8b33a Mon Sep 17 00:00:00 2001 From: awkawb Date: Sun, 17 Dec 2023 10:05:51 -0500 Subject: [PATCH] Initial commit --- CHANGELOG.md | 0 README.md | 5 ++ invoice_ninja/__init__.py | 74 +++++++++++++++++++ invoice_ninja/endpoints/__init__.py | 0 invoice_ninja/endpoints/base_endpoint.py | 30 ++++++++ invoice_ninja/endpoints/clients.py | 92 ++++++++++++++++++++++++ invoice_ninja/models/README.md | 0 invoice_ninja/models/__init__.py | 0 invoice_ninja/models/client.py | 27 +++++++ invoice_ninja/models/clientContact.py | 36 ++++++++++ invoice_ninja/models/clientSettings.py | 38 ++++++++++ shell.nix | 13 ++++ 12 files changed, 315 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 invoice_ninja/__init__.py create mode 100644 invoice_ninja/endpoints/__init__.py create mode 100644 invoice_ninja/endpoints/base_endpoint.py create mode 100644 invoice_ninja/endpoints/clients.py create mode 100644 invoice_ninja/models/README.md create mode 100644 invoice_ninja/models/__init__.py create mode 100644 invoice_ninja/models/client.py create mode 100644 invoice_ninja/models/clientContact.py create mode 100644 invoice_ninja/models/clientSettings.py create mode 100644 shell.nix diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c91126 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Python Invoice Ninja SDK + +Inspired by the [official PHP SDK](https://github.com/invoiceninja/sdk-php), a +Python wrapper for Invoice Ninja's REST API. + diff --git a/invoice_ninja/__init__.py b/invoice_ninja/__init__.py new file mode 100644 index 0000000..1d3c3f9 --- /dev/null +++ b/invoice_ninja/__init__.py @@ -0,0 +1,74 @@ +import requests + +class InvoiceNinja(object): + API_V1 = 'api/v1' + ENDPOINT_URLS = { + 'clients': 'clients', + 'products': 'products', + 'invoices': 'invoices', + 'recurring invoices': 'recurring_invoices', + 'payments': 'payments', + 'quotes': 'quotes', + 'credits': 'credits', + 'reports': 'reports', + 'activities': 'activities', + 'charts': 'charts', + 'companies': 'companies', + 'documents': 'documents', + 'emails': 'emails', + 'expense': 'expenses', + 'export': 'export', + 'import': 'import_json', + 'ping': 'ping', + 'health check': 'health_check', + 'users': 'users' + } + + def __init__(self, + endpoint_url: str = 'https://invoicing.co', + api_token: str = str()): + self.endpoint_url = '{}/{}'.format(endpoint_url, self.API_V1) + self.api_token = api_token + self.headers = dict() + + def _get_url_for(self, endpoint: str = 'ping'): + ''' + Get complete URL for an endpoint. + + Endpoint URLs are appended to the Invoice Ninja base URL + and returned. + ''' + if endpoint in self.ENDPOINT_URLS: + return '{}/{}'.format(self.endpoint_url, + self.ENDPOINT_URLS[endpoint]) + + else: + raise KeyError('Endpoint URL not found') + + #def _get_headers(self, headers: dict = dict()): + def build_headers(self): + ''' + Build Invoice Ninja API headers for request. + + A header dictionary with the API token is returned by default. + ''' + + headers = { + 'X-API-TOKEN': self.api_token, + 'X-Requested-With': 'XMLHttpRequest'} + + return self.headers.update(headers) + + def ping(self): + ''' + Ping Invoice Ninja instance. + ''' + server_response = requests.get(url=self._get_url_for(), + headers=self.build_headers()) + + if server_response.ok: + return True + + else: + return False + diff --git a/invoice_ninja/endpoints/__init__.py b/invoice_ninja/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_ninja/endpoints/base_endpoint.py b/invoice_ninja/endpoints/base_endpoint.py new file mode 100644 index 0000000..2415307 --- /dev/null +++ b/invoice_ninja/endpoints/base_endpoint.py @@ -0,0 +1,30 @@ +from invoice_ninja import InvoiceNinja + +class BaseEndpoint(InvoiceNinja): + def bulk(self, action: str): + pass + + def archive(self): + pass + + def delete(self): + pass + + def restore(self): + pass + + def all(self): + pass + + def get(self): + pass + + def update(self): + pass + + def create(self): + pass + + def download(self): + pass + diff --git a/invoice_ninja/endpoints/clients.py b/invoice_ninja/endpoints/clients.py new file mode 100644 index 0000000..b7d5397 --- /dev/null +++ b/invoice_ninja/endpoints/clients.py @@ -0,0 +1,92 @@ +from invoice_ninja.endpoints.base_endpoint import BaseEndpoint +from invoice_ninja.types.client import Client + +import requests + +class Clients(BaseEndpoint): + uri = '/api/v1/clients' + + def __init__(self, base_url: str = str(), api_token: str = str()): + super().__init__(base_url, api_token) + self.url = super()._get_url_for('clients') + + def __build_sort_params(self, sort: dict): + sort_params = {'sort': str()} + is_first_entry = True + for option in sort.keys(): + if is_first_entry: + sort_params['sort'] += '{}|{}'.format(option, sort[option]) + is_first_entry = False + + else: + sort_params['sort'] += ' {}|{}'.format(option, sort[option]) + + return sort_params + + def __client_from_dict(self, client: dict): + return Client(client_id=client['id'], + name=client['name'], + address=client['address1'], + city=client['city'], + state=client['state'], + postal_code=client['postal_code'], + phone=client['phone'], + email=client['contacts'][0]['email'], + pets=client['custom_value1']) + + def __clients_from_response(self, response: requests.Response): + clients = list() + for client_dict in response.json()['data']: + clients.append(self.__client_from_dict(client_dict)) + + return clients + + def show_client(self, client_id: str = None): + """ + Get client based on client id. + """ + + if client_id: + response = requests.get(url=self.url, + headers=super()._get_headers()) + + if response.ok: + return self.__client_from_dict(response.json()['data']) + + return None + + def list_clients(self, include: str = 'activities', + sort: dict = dict(), status: str = 'active', + name: str = None): + """ + Get list of clients. + """ + + request_params = dict() + + # Add sort parameters to request + if len(sort) > 0: + request_params.update(self.__build_sort_params(sort)) + + # Add include parameters to request + request_params.update({'include': include}) + + # Add status parameters to request + request_params.update({'status': status}) + + # Add name parameters to request + if name: + request_params.update({'name': name}) + + # Check is request should be sent with parameters + if len(request_params) > 0: + response = requests.get(url=self.url, + params=request_params, + headers=super()._get_headers()) + else: + response = requests.get(url=self.url, + headers=super()._get_headers()) + + if response.ok: + return self.__clients_from_response(response) + diff --git a/invoice_ninja/models/README.md b/invoice_ninja/models/README.md new file mode 100644 index 0000000..e69de29 diff --git a/invoice_ninja/models/__init__.py b/invoice_ninja/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_ninja/models/client.py b/invoice_ninja/models/client.py new file mode 100644 index 0000000..1d7d277 --- /dev/null +++ b/invoice_ninja/models/client.py @@ -0,0 +1,27 @@ +class Client(object): + def __init__(self, client_id: int = None, name: str = None, + address: str = None, city: str = None, state: str = None, + postal_code: str = None, phone: str = None, + email: str = None, pets: str = None): + self.id = client_id + self.name = name + self.address = address + self.city = city + self.state = state + self.postal_code = postal_code + self.phone = phone + self.email = email + self.pets = pets + + def __str__(self): + return 'Client({}, {}, {}, {}, {}, {}, {}, {}, {})'.format( + self.id, + self.name, + self.address, + self.city, + self.state, + self.postal_code, + self.phone, + self.email, + self.pets) + diff --git a/invoice_ninja/models/clientContact.py b/invoice_ninja/models/clientContact.py new file mode 100644 index 0000000..93a7fe0 --- /dev/null +++ b/invoice_ninja/models/clientContact.py @@ -0,0 +1,36 @@ +class ClientContact(object): + def __init__(self, first_name: str = '', + last_name: str = '', + email: str = '', + phone: str = '', + send_email: bool = True, + custom_value1: str = '', + custom_value2: str = '', + custom_value3: str = '', + custom_value4: str = ''): + self.first_name = first_name + self.last_name = last_name + self.email = email + self.phone = phone + + # Flag for whether the contact will receive emails. + self.send_email = send_email + + # Custom values + self.custom_value1 = custom_value1 + self.custom_value2 = custom_value2 + self.custom_value3 = custom_value3 + self.custom_value4 = custom_value4 + + def __str__(self): + return 'ClientContact({}, {}, {}, {}, {}, {}, {}, {}, {})'.format( + self.first_name, + self.last_name, + self.email, + self.phone, + self.send_email, + self.custom_value1, + self.custom_value2, + self.custom_value3, + self.custom_value4) + diff --git a/invoice_ninja/models/clientSettings.py b/invoice_ninja/models/clientSettings.py new file mode 100644 index 0000000..ba9fa4a --- /dev/null +++ b/invoice_ninja/models/clientSettings.py @@ -0,0 +1,38 @@ +class ClientSettings(object): + def __init__(self, language_id: str = None, + currency_id: str = None, + payment_terms: str = None, + valid_until: str = None, + default_task_rate: float = 0, + send_reminders: bool = None): + # The language ID for the client, for the full list of languages, + # please see this resource - optional + # + # https://invoiceninja.github.io/docs/statics/#languages + self.language_id = language_id + + # The currency ID - optional + # See this resource for full list: + # + # https://invoiceninja.github.io/docs/statics/#currencies + self.currency_id = currency_id + + # The payment terms - in days - optional + self.payment_terms = payment_terms + + # The quote terms - optional + # + # How many days the quote will be valid for. + self.valid_until = valid_until + + # The task rate for this client - optional + # + # A value of 0 equates to disabled. + self.default_task_rate = default_task_rate + + # Whether the client will receive reminders - optional + # + # When left unset, this setting will rely on the company + # settings as the override/default + self.send_reminders = send_reminders + diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..5c95e95 --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +{ pkgs ? import {} }: + + +with pkgs; +pkgs.mkShell { + nativeBuildInputs = [ + # Python development environment + (python3.withPackages(ps: with ps; [ + requests + ])) + ]; +} +