Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d35f85c1b | |||
| 3eaa187019 | |||
| d274bdad49 | |||
| ebc55fbf8d | |||
| aefb0dc2ab | |||
| 249cf62fcc | |||
| 8e70ce3c26 | |||
| 23d2999baa | |||
| 05c0ccb398 | |||
| deacda0cfa | |||
| cc34db9ed1 | |||
| 713613e2c6 | |||
| a9b38d8515 | |||
| 79b4dfd4eb | |||
| d8af2d0287 |
33
.editorconfig
Normal file
33
.editorconfig
Normal file
@ -0,0 +1,33 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8
|
||||
|
||||
# Python indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Nix indentation
|
||||
[*.nix]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
10
.gitea/workflows/test_clients.yaml
Normal file
10
.gitea/workflows/test_clients.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
name: test-clients
|
||||
run-name: ${{ gitea.actor }} is testing pyinvoiceninja
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
pyinvoiceninja:
|
||||
runs-on: invoiceninja
|
||||
steps:
|
||||
- run: echo "Hello world!"
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@ -0,0 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- This CHANGELOG.md!
|
||||
22
README.md
22
README.md
@ -1,5 +1,25 @@
|
||||
# Python Invoice Ninja SDK
|
||||
# pyinvoiceninja
|
||||
|
||||
Inspired by the [official PHP SDK](https://github.com/invoiceninja/sdk-php), a
|
||||
Python wrapper for Invoice Ninja's REST API.
|
||||
|
||||
## Model Projects
|
||||
|
||||
Projects to look to for guidance:
|
||||
|
||||
* [official PHP SDK](https://github.com/invoiceninja/sdk-php)
|
||||
- official Invoice Ninja SDK
|
||||
|
||||
* [python-vaultwarden](https://github.com/numberly/python-vaultwarden)
|
||||
- simple python wrapper for the bitwarden/vaultwarden
|
||||
|
||||
## How to use
|
||||
|
||||
Let see some basic usage:
|
||||
```
|
||||
>>> # Import client from module
|
||||
>>> from pyinvoiceninja import InvoiceNinjaClient
|
||||
>>> #
|
||||
>>> client = InvoiceNinjaClient(base_url='https://ninja.instance/api/v1')
|
||||
```
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
from invoice_ninja.endpoints.clients import Clients
|
||||
|
||||
class InvoiceNinja(object):
|
||||
def __init__(self, endpoint_url: str = 'https://invoicing.co',
|
||||
api_token: str = str()):
|
||||
self._endpoint_url = endpoint_url
|
||||
self._api_token = api_token
|
||||
|
||||
@property
|
||||
def endpoint_url(self):
|
||||
return self._endpoint_url
|
||||
|
||||
@endpoint_url.setter
|
||||
def endpoint_url(self, endpoint_url: str):
|
||||
self._endpoint_url = endpoint_url
|
||||
|
||||
@property
|
||||
def api_token(self):
|
||||
return self._api_token
|
||||
|
||||
@api_token.setter
|
||||
def api_token(self, api_token: str):
|
||||
self._api_token = api_token
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
class BaseEndpoint(object):
|
||||
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
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
from invoice_ninja.endpoints.base_endpoint import BaseEndpoint
|
||||
from invoice_ninja.models.client import Client
|
||||
|
||||
import requests
|
||||
|
||||
class Clients(BaseEndpoint):
|
||||
uri = '/api/v1/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)
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
from invoice_ninja.endpoints.base_endpoint import BaseEndpoint
|
||||
|
||||
import requests
|
||||
|
||||
class Ping(BaseEndpoint):
|
||||
uri = '/api/v1/ping'
|
||||
|
||||
def ping(self):
|
||||
pass
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import requests
|
||||
|
||||
class HTTPClient(object):
|
||||
"""HTTP client for Invoice Ninja REST API."""
|
||||
|
||||
def __init__(self, endpoint_url: str = 'https://invoicing.co',
|
||||
api_token: str = str()):
|
||||
self._endpoint_url = 'https://invoicing.co'
|
||||
self._api_token = str()
|
||||
self._headers = dict()
|
||||
|
||||
@property
|
||||
def endpoint_url(self):
|
||||
return self._endpoint_url
|
||||
|
||||
@endpoint_url.setter
|
||||
def endpoint_url(self, endpoint_url: str):
|
||||
self._endpoint_url = endpoint_url
|
||||
|
||||
@property
|
||||
def api_token(self):
|
||||
return self._api_token
|
||||
|
||||
@api_token.setter
|
||||
def api_token(self, api_token: str):
|
||||
self._api_token = api_token
|
||||
|
||||
def add_headers(self, headers: dict):
|
||||
"""Add HTTP headers to request."""
|
||||
|
||||
self._headers.update(headers)
|
||||
|
||||
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': _api_token,
|
||||
'X-Requested-With': 'XMLHttpRequest'}
|
||||
|
||||
return self._headers.update(headers)
|
||||
|
||||
def send(self, uri: str,
|
||||
payload: dict,
|
||||
method: str = 'get'):
|
||||
"""Send request to Invoice Ninja REST API."""
|
||||
|
||||
url = '{}/{}'.format(endpoint_url, uri)
|
||||
|
||||
if method == 'get':
|
||||
return requests.get(url, params=payload)
|
||||
|
||||
elif method == 'post':
|
||||
return requests.post(url, params=payload)
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import requests
|
||||
|
||||
class HTTPClient(object):
|
||||
"""HTTP client for Invoice Ninja REST API."""
|
||||
|
||||
def __init__(self, endpoint_url: str = 'https://invoicing.co',
|
||||
api_token: str = str()):
|
||||
self_endpoint_url = 'https://invoicing.co'
|
||||
self._api_token = str()
|
||||
self._headers = dict()
|
||||
|
||||
@property
|
||||
def endpoint_url(self):
|
||||
return self._endpoint_url
|
||||
|
||||
@endpoint_url.setter
|
||||
def endpoint_url(self, endpoint_url: str):
|
||||
self._endpoint_url = endpoint_url
|
||||
|
||||
@property
|
||||
def api_token(self):
|
||||
return self._api_token
|
||||
|
||||
@api_token.setter
|
||||
def api_token(self, api_token: str):
|
||||
self._api_token = api_token
|
||||
|
||||
def add_headers(self, headers: dict):
|
||||
"""Add HTTP headers to request."""
|
||||
|
||||
self._headers.update(headers)
|
||||
|
||||
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': _api_token,
|
||||
'X-Requested-With': 'XMLHttpRequest'}
|
||||
|
||||
return self._headers.update(headers)
|
||||
|
||||
def send(self, uri: str,
|
||||
payload: dict,
|
||||
method: str = 'get'):
|
||||
"""Send request to Invoice Ninja REST API."""
|
||||
|
||||
url = '{}/{}'.format(endpoint_url, uri)
|
||||
|
||||
if method == 'get':
|
||||
return requests.get(url, params=payload)
|
||||
|
||||
elif method == 'post':
|
||||
return requests.post(url, params=payload)
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
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)
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
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)
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
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
|
||||
|
||||
4
pyinvoiceninja/__init__.py
Normal file
4
pyinvoiceninja/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .client import InvoiceNinjaClient
|
||||
|
||||
__all__ = ['InvoiceNinjaClient']
|
||||
|
||||
146
pyinvoiceninja/client.py
Normal file
146
pyinvoiceninja/client.py
Normal file
@ -0,0 +1,146 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
# HTTP client
|
||||
import requests
|
||||
|
||||
# Logger
|
||||
from loguru import logger
|
||||
|
||||
# Local library imports
|
||||
from .endpoints.clients import ClientsAPI
|
||||
from .endpoints.documents import DocumentsAPI
|
||||
|
||||
class InvoiceNinjaClient(object):
|
||||
"""
|
||||
A client for interacting with the Invoice Ninja API.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
base_url : str
|
||||
URL to Invoice Ninja instance.
|
||||
api_token : str
|
||||
Token for API request access.
|
||||
clients : ClientsAPI
|
||||
An instance of the ClientsAPI class for client-related operations.
|
||||
documents : DocumentsAPI
|
||||
An instance of the DocumentsAPI class for document-related operations.
|
||||
debug : bool
|
||||
Flag to enable or disable debug logging.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
base_url: str = 'https://invoicing.co/api/v1',
|
||||
api_token: str = '',
|
||||
debug: bool = False):
|
||||
"""
|
||||
Constructs all the necessary attributes for the InvoiceNinjaClient object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
base_url : str, optional
|
||||
The base URL of the Invoice Ninja API (default is 'https://invoicing.co/api/v1').
|
||||
api_token : str, optional
|
||||
The API token for authentication (default is an empty string).
|
||||
debug : bool, optional
|
||||
Flag to enable or disable debug logging (default is False).
|
||||
"""
|
||||
|
||||
# Check parameters for correct types
|
||||
if not isinstance(base_url, str):
|
||||
raise ValueError('Base URL must be a string.')
|
||||
if not isinstance(api_token, str):
|
||||
raise ValueError('API token must be a string.')
|
||||
if not isinstance(debug, bool):
|
||||
raise ValueError('Debug flag must be a boolean.')
|
||||
|
||||
# Initialize paramater attributes
|
||||
self._base_url = base_url
|
||||
self._api_token = api_token
|
||||
self._debug = debug
|
||||
|
||||
# Initialize API client attributes
|
||||
self._documents = DocumentsAPI(self)
|
||||
self._clients = ClientsAPI(self)
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
"""Gets the base URL of the Invoice Ninja API."""
|
||||
return self._base_url
|
||||
|
||||
@base_url.setter
|
||||
def base_url(self, base_url: str):
|
||||
"""Sets the base URL of the Invoice Ninja API."""
|
||||
if not isinstance(base_url, str):
|
||||
raise ValueError('Base URL must be a string.')
|
||||
self._base_url = base_url
|
||||
|
||||
@property
|
||||
def api_token(self) -> str:
|
||||
"""Gets the API token."""
|
||||
return self._api_token
|
||||
|
||||
@api_token.setter
|
||||
def api_token(self, api_token: str):
|
||||
"""Sets the API token."""
|
||||
if not isinstance(api_token, str):
|
||||
raise ValueError('API token must be a string.')
|
||||
self._api_token = api_token
|
||||
|
||||
@property
|
||||
def clients(self) -> ClientsAPI:
|
||||
return self._clients
|
||||
|
||||
@property
|
||||
def documents(self) -> DocumentsAPI:
|
||||
return self._documents
|
||||
|
||||
@property
|
||||
def debug(self):
|
||||
return self._debug
|
||||
|
||||
@debug.setter
|
||||
def debug(self, debug: bool) -> None:
|
||||
"""Sets the debug flag."""
|
||||
if not isinstance(debug, bool):
|
||||
raise ValueError('Debug flag must be a boolean.')
|
||||
self._debug = debug
|
||||
if self._debug:
|
||||
logger.enable(__name__)
|
||||
else:
|
||||
logger.disable(__name__)
|
||||
|
||||
@property
|
||||
def headers(self) -> Dict[str, Any]:
|
||||
"""Gets the headers for API requests."""
|
||||
return {'X-API-TOKEN': self._api_token,
|
||||
'X-Requested-With': 'XMLHttpRequest'}
|
||||
|
||||
def _log_debug(self, message: str) -> None:
|
||||
"""
|
||||
Logs a debug message if debugging is enabled.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message : str
|
||||
The debug message to log.
|
||||
"""
|
||||
if self._debug:
|
||||
print(f'DEBUG: {message}')
|
||||
|
||||
def ping(self):
|
||||
"""
|
||||
Ping the Invoice Ninja instance to check if it's reachable.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the server response is OK, False otherwise.
|
||||
"""
|
||||
self._log_debug(f'Pinging {self.base_url}/ping with headers {self.headers}')
|
||||
server_response = requests.get(url=f'{self.base_url}/ping',
|
||||
headers=self.headers)
|
||||
self._log_debug(f'Server response: {server_response.status_code}, {server_response.text}')
|
||||
|
||||
return server_response.ok
|
||||
|
||||
5
pyinvoiceninja/endpoints/__init__.py
Normal file
5
pyinvoiceninja/endpoints/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .clients import ClientsAPI
|
||||
from .documents import DocumentsAPI
|
||||
|
||||
__all__ = ['ClientsAPI', 'DocumentsAPI']
|
||||
|
||||
176
pyinvoiceninja/endpoints/clients.py
Normal file
176
pyinvoiceninja/endpoints/clients.py
Normal file
@ -0,0 +1,176 @@
|
||||
# HTTP client
|
||||
import requests
|
||||
|
||||
# Logger
|
||||
from loguru import logger
|
||||
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from pyinvoiceninja.models import Client
|
||||
|
||||
class ClientsAPI:
|
||||
"""
|
||||
A client for interacting with the Invoice Ninja clients API.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
client : InvoiceNinjaClient
|
||||
An instance of the InvoiceNinjaClient class.
|
||||
clients_url : str
|
||||
The URL for accessing the clients endpoint.
|
||||
"""
|
||||
|
||||
VALID_SORT_BY: List[str] = [
|
||||
'id', 'name', 'balance', 'paid_to_date', 'payment_balance',
|
||||
'credit_balance', 'archived_at', 'updated_at', 'created_at',
|
||||
'display_name', 'address', 'city', 'state', 'postal_code',
|
||||
'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4'
|
||||
]
|
||||
VALID_SORT_ORDER: List[str] = ['asc', 'desc']
|
||||
VALID_INCLUDES: List[str] = ['activities', 'ledger', 'system_logs']
|
||||
VALID_CLIENT_STATUSES: List[str] = ['active', 'archived', 'deleted']
|
||||
VALID_BALANCE_OPERATORS: List[str] = ['lt', 'lte', 'gt', 'gte', 'eq']
|
||||
|
||||
def __init__(self, client: 'InvoiceNinjaClient') -> None:
|
||||
"""
|
||||
Constructs all the necessary attributes for the ClientsAPI object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
client : InvoiceNinjaClient
|
||||
An instance of the InvoiceNinjaClient class.
|
||||
"""
|
||||
self.client = client
|
||||
self.clients_url = f'{self.client.base_url}/clients'
|
||||
|
||||
def get_clients(self,
|
||||
includes: Optional[List[str]] = None,
|
||||
sort: Optional[Dict[str, str]] = None,
|
||||
statuses: Optional[List[str]] = None,
|
||||
name: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
balance: Optional[Dict[str, Union[str, int]]] = None,
|
||||
is_deleted: Optional[bool] = None,
|
||||
filter_deleted_clients: Optional[bool] = None) -> List[Client]:
|
||||
"""
|
||||
Retrieves a list of clients from the Invoice Ninja API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
includes : Optional[List[str]]
|
||||
The child relationships to include in the response.
|
||||
sort : Optional[Dict[str, str]]
|
||||
A dictionary containing the field by which to sort the clients and the order.
|
||||
statuses : Optional[List[str]]
|
||||
The statuses to filter the clients.
|
||||
name : Optional[str]
|
||||
The name to filter the clients.
|
||||
email : Optional[str]
|
||||
The email to filter the clients.
|
||||
balance : Optional[Dict[str, Union[str, int]]]
|
||||
The balance to filter the clients.
|
||||
is_deleted : Optional[bool]
|
||||
Whether to include deleted clients.
|
||||
filter_deleted_clients : Optional[bool]
|
||||
Whether to filter out deleted clients.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[Client]
|
||||
A list of Client objects if the request is successful.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If any of the parameters are invalid.
|
||||
"""
|
||||
params = {}
|
||||
|
||||
if includes:
|
||||
if isinstance(includes, list):
|
||||
for item in includes:
|
||||
if item not in self.VALID_INCLUDES:
|
||||
raise ValueError(f'Invalid include option: {item}. Valid options: {self.VALID_INCLUDES}')
|
||||
params['include'] = ','.join(includes)
|
||||
else:
|
||||
raise ValueError('Includes must be a list of valid client child relationship strings.')
|
||||
|
||||
if sort:
|
||||
if isinstance(sort, dict):
|
||||
if 'sort_by' not in sort or sort['sort_by'] not in self.VALID_SORT_BY:
|
||||
raise ValueError(f'Invalid sort by option: {sort["sort_by"]}. Valid options: {self.VALID_SORT_BY}')
|
||||
if 'sort_order' not in sort or sort['sort_order'] not in self.VALID_SORT_ORDER:
|
||||
raise ValueError(f'Invalid sort order option: {sort["sort_order"]}. Valid options: {self.VALID_SORT_ORDER}')
|
||||
params['sort'] = f'{sort["sort_by"]}|{sort["sort_order"]}'
|
||||
else:
|
||||
raise ValueError('Sort must be a dictionary with "sort_by" and "sort_order" keys.')
|
||||
|
||||
if statuses:
|
||||
if isinstance(statuses, list):
|
||||
for item in statuses:
|
||||
if item not in self.VALID_CLIENT_STATUSES:
|
||||
raise ValueError(f'Invalid status: {item}. Valid options: {self.VALID_CLIENT_STATUSES}')
|
||||
params['status'] = ','.join(statuses)
|
||||
else:
|
||||
raise ValueError('Statuses must be a list of valid client statuses.')
|
||||
|
||||
if name:
|
||||
if isinstance(name, str):
|
||||
params['name'] = name
|
||||
else:
|
||||
raise ValueError('Name must be a string.')
|
||||
|
||||
if email:
|
||||
if isinstance(email, str):
|
||||
params['email'] = email
|
||||
else:
|
||||
raise ValueError('Email parameter must be a string value.')
|
||||
|
||||
# FIXME: This doesn't seem to be working properly.
|
||||
if balance:
|
||||
if isinstance(balance, dict):
|
||||
if 'operator' in balance and 'value' in balance:
|
||||
if balance['operator'] not in self.VALID_BALANCE_OPERATORS:
|
||||
raise ValueError(f'Invalid balance operator: {balance["operator"]}. Valid options: {self.VALID_BALANCE_OPERATORS}')
|
||||
if not isinstance(balance['value'], int):
|
||||
raise ValueError('Balance value must be an integer.')
|
||||
params['balance'] = f'{balance["operator"]}:{balance["value"]}'
|
||||
else:
|
||||
raise ValueError('Balance must be a dictionary with "operator" and "value" keys.')
|
||||
else:
|
||||
raise ValueError('Balance must be a dictionary with "operator" and "value" keys.')
|
||||
|
||||
if is_deleted is not None:
|
||||
if isinstance(is_deleted, bool):
|
||||
params['is_deleted'] = is_deleted
|
||||
else:
|
||||
raise ValueError('Is deleted must be a boolean value.')
|
||||
|
||||
if filter_deleted_clients is not None:
|
||||
if isinstance(filter_deleted_clients, bool):
|
||||
params['filter_deleted_clients'] = filter_deleted_clients
|
||||
else:
|
||||
raise ValueError('Filter deleted clients must be a boolean value.')
|
||||
|
||||
request = requests.Request('GET', self.clients_url, headers=self.client.headers, params=params)
|
||||
prepared_request = request.prepare()
|
||||
logger.debug(f'Fetching clients from server: {prepared_request.url}')
|
||||
session = requests.Session()
|
||||
response = session.send(prepared_request)
|
||||
logger.debug(f'Server response status: {response.status_code}')
|
||||
|
||||
if response.ok:
|
||||
clients = [Client.from_json(client_dict) for client_dict in response.json().get('data', [])]
|
||||
if clients:
|
||||
logger.success('Fetched client list from server.')
|
||||
return clients
|
||||
else:
|
||||
logger.error('Search yielded no clients.')
|
||||
return []
|
||||
else:
|
||||
logger.error(f'Error fetching clients: {response.text}')
|
||||
response.raise_for_status()
|
||||
|
||||
64
pyinvoiceninja/endpoints/documents.py
Normal file
64
pyinvoiceninja/endpoints/documents.py
Normal file
@ -0,0 +1,64 @@
|
||||
import requests
|
||||
|
||||
from pyinvoiceninja.models import Client
|
||||
from pyinvoiceninja.models import Document
|
||||
|
||||
class DocumentsAPI(object):
|
||||
"""
|
||||
A client for interacting with the Invoice Ninja documents API.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
client : InvoiceNinjaClient
|
||||
An instance of the InvoiceNinjaClient class.
|
||||
documents_url : str
|
||||
The URL for accessing the clients endpoint.
|
||||
"""
|
||||
|
||||
def __init__(self, client):
|
||||
"""
|
||||
Constructs all the necessary attributes for the DocumentsAPI object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
client : InvoiceNinjaClient
|
||||
An instance of the InvoiceNinjaClient class.
|
||||
"""
|
||||
self.client = client
|
||||
self.documents_url = f'{self.client.base_url}/documents'
|
||||
|
||||
def __get_documents_from_response(self, response: requests.Response):
|
||||
"""
|
||||
Parses the response from the API and converts it to a list of Client objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
response : requests.Response
|
||||
The response object from the API request.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of Client
|
||||
A list of Client objects parsed from the response.
|
||||
"""
|
||||
documents = list()
|
||||
for document_dict in response.json()['data']:
|
||||
documents.append(Document.from_json(document_dict))
|
||||
return documents
|
||||
|
||||
def get_documents(self):
|
||||
"""
|
||||
Retrieves a list of documents from the Invoice Ninja API.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of Documents
|
||||
A list of Document objects if the request is successful.
|
||||
"""
|
||||
self.client._log_debug(f'Fetching documents from {self.documents_url}')
|
||||
response = requests.get(self.documents_url, headers=self.client.headers)
|
||||
self.client._log_debug(f'Server response: {response.status_code}, {response.text}')
|
||||
|
||||
if response.ok:
|
||||
return self.__get_documents_from_response(response)
|
||||
|
||||
7
pyinvoiceninja/models/__init__.py
Normal file
7
pyinvoiceninja/models/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from .client import Client
|
||||
from .clientContact import ClientContact
|
||||
from .clientSettings import ClientSettings
|
||||
from .document import Document
|
||||
|
||||
__all__ = ['Client', 'ClientContact', 'ClientSettings', 'Document']
|
||||
|
||||
402
pyinvoiceninja/models/client.py
Normal file
402
pyinvoiceninja/models/client.py
Normal file
@ -0,0 +1,402 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
|
||||
# JSON serializer/deserializer
|
||||
import json
|
||||
|
||||
# Local library imports
|
||||
from pyinvoiceninja.models.clientContact import ClientContact
|
||||
from pyinvoiceninja.models.clientInvoice import ClientInvoice
|
||||
from pyinvoiceninja.models.clientPayment import ClientPayment
|
||||
from pyinvoiceninja.models.clientSettings import ClientSettings
|
||||
from pyinvoiceninja.models.document import Document
|
||||
|
||||
class Client:
|
||||
"""
|
||||
A class to represent a client.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the client.
|
||||
name (str): The name of the client.
|
||||
private_notes (str): Private notes about the client.
|
||||
public_notes (str): Public notes about the client.
|
||||
balance (int): The balance of the client.
|
||||
paid_to_date (int): The amount paid to date by the client.
|
||||
payment_balance (int): The payment balance of the client.
|
||||
credit_balance (int): The credit balance of the client.
|
||||
settings (dict): Client-specific settings.
|
||||
is_deleted (bool): Indicates if the client is deleted.
|
||||
updated_at (int): The timestamp when the client was last updated.
|
||||
archived_at (int): The timestamp when the client was archived.
|
||||
created_at (int): The timestamp when the client was created.
|
||||
display_name (str): The display name of the client.
|
||||
contacts (list): A list of contacts associated with the client.
|
||||
documents (list): A list of documents associated with the client.
|
||||
address (str): The address of the client.
|
||||
city (str): The city of the client.
|
||||
state (str): The state of the client.
|
||||
postal_code (str): The postal code of the client.
|
||||
phone (str): The phone number of the client.
|
||||
custom_value1 (str): Custom field 1.
|
||||
custom_value2 (str): Custom field 2.
|
||||
custom_value3 (str): Custom field 3.
|
||||
custom_value4 (str): Custom field 4.
|
||||
"""
|
||||
|
||||
def __init__(self, client_id: int = 0,
|
||||
name: str = '',
|
||||
private_notes: str = '',
|
||||
public_notes: str = '',
|
||||
balance: int = 0,
|
||||
paid_to_date: int = 0,
|
||||
payment_balance: int = 0,
|
||||
credit_balance: int = 0,
|
||||
settings: Optional[ClientSettings] = None,
|
||||
is_deleted: bool = False,
|
||||
updated_at: int = 0,
|
||||
archived_at: int = 0,
|
||||
created_at: int = 0,
|
||||
display_name: str = '',
|
||||
contacts: Optional[List[ClientContact]] = None,
|
||||
documents: Optional[List[Document]] = None,
|
||||
address: str = '',
|
||||
city: str = '',
|
||||
state: str = '',
|
||||
postal_code: str = '',
|
||||
phone: str = '',
|
||||
custom_value1: str = '',
|
||||
custom_value2: str = '',
|
||||
custom_value3: str = '',
|
||||
custom_value4: str = '',
|
||||
ledger: Optional[List[Union[ClientPayment, ClientInvoice]]] = None):
|
||||
self._id = client_id
|
||||
self._name = name
|
||||
self._private_notes = private_notes
|
||||
self._public_notes = public_notes
|
||||
self._balance = balance
|
||||
self._paid_to_date = paid_to_date
|
||||
self._payment_balance = payment_balance
|
||||
self._credit_balance = credit_balance
|
||||
self._settings = settings or {}
|
||||
self._is_deleted = is_deleted
|
||||
self._updated_at = updated_at
|
||||
self._archived_at = archived_at
|
||||
self._created_at = created_at
|
||||
self._display_name = display_name
|
||||
self._contacts = contacts or []
|
||||
self._documents = documents or []
|
||||
self._address = address
|
||||
self._city = city
|
||||
self._state = state
|
||||
self._postal_code = postal_code
|
||||
self._phone = phone
|
||||
self._custom_value1 = custom_value1
|
||||
self._custom_value2 = custom_value2
|
||||
self._custom_value3 = custom_value3
|
||||
self._custom_value4 = custom_value4
|
||||
self._ledger = ledger
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self._name = name
|
||||
|
||||
@name.deleter
|
||||
def name(self):
|
||||
self._name = ''
|
||||
|
||||
@property
|
||||
def private_notes(self):
|
||||
return self._private_notes
|
||||
|
||||
@private_notes.setter
|
||||
def private_notes(self, note):
|
||||
self._private_notes = note
|
||||
|
||||
@private_notes.deleter
|
||||
def private_notes(self):
|
||||
self._private_notes = ''
|
||||
|
||||
@property
|
||||
def public_notes(self):
|
||||
return self._public_notes
|
||||
|
||||
@public_notes.setter
|
||||
def public_notes(self, note):
|
||||
self._public_notes = note
|
||||
|
||||
@public_notes.deleter
|
||||
def public_notes(self):
|
||||
self._public_notes = ''
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
return self._balance
|
||||
|
||||
@property
|
||||
def paid_to_date(self):
|
||||
return self._paid_to_date
|
||||
|
||||
@property
|
||||
def payment_balance(self):
|
||||
return self._payment_balance
|
||||
|
||||
@property
|
||||
def credit_balance(self):
|
||||
return self._credit_balance
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
@settings.setter
|
||||
def settings(self, settings):
|
||||
self._settings = settings
|
||||
|
||||
@settings.deleter
|
||||
def settings(self):
|
||||
self._settings = {}
|
||||
|
||||
@property
|
||||
def is_deleted(self):
|
||||
return self._is_deleted
|
||||
|
||||
@property
|
||||
def updated_at(self):
|
||||
return self._updated_at
|
||||
|
||||
@property
|
||||
def archived_at(self):
|
||||
return self._archived_at
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
return self._created_at
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._display_name
|
||||
|
||||
@display_name.setter
|
||||
def display_name(self, name):
|
||||
self._display_name = name
|
||||
|
||||
@display_name.deleter
|
||||
def display_name(self):
|
||||
self._display_name = ''
|
||||
|
||||
@property
|
||||
def contacts(self):
|
||||
return self._contacts
|
||||
|
||||
@contacts.setter
|
||||
def contacts(self, contacts):
|
||||
self._contacts = contacts
|
||||
|
||||
@contacts.deleter
|
||||
def contacts(self):
|
||||
self._contacts = []
|
||||
|
||||
@property
|
||||
def documents(self):
|
||||
return self._documents
|
||||
|
||||
@documents.setter
|
||||
def documents(self, documents):
|
||||
self._documents = documents
|
||||
|
||||
@documents.deleter
|
||||
def documents(self):
|
||||
self._documents = []
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return self._address
|
||||
|
||||
@address.setter
|
||||
def address(self, address):
|
||||
self._address = address
|
||||
|
||||
@address.deleter
|
||||
def address(self):
|
||||
self._address = ''
|
||||
|
||||
@property
|
||||
def city(self):
|
||||
return self._city
|
||||
|
||||
@city.setter
|
||||
def city(self, city):
|
||||
self._city = city
|
||||
|
||||
@city.deleter
|
||||
def city(self):
|
||||
self._city = ''
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self._state = state
|
||||
|
||||
@state.deleter
|
||||
def state(self):
|
||||
self._state = ''
|
||||
|
||||
@property
|
||||
def postal_code(self):
|
||||
return self._postal_code
|
||||
|
||||
@postal_code.setter
|
||||
def postal_code(self, postal_code):
|
||||
self._postal_code = postal_code
|
||||
|
||||
@postal_code.deleter
|
||||
def postal_code(self):
|
||||
self._postal_code = ''
|
||||
|
||||
@property
|
||||
def custom_value1(self):
|
||||
return self._custom_value1
|
||||
|
||||
@custom_value1.setter
|
||||
def custom_value1(self, value):
|
||||
self._custom_value1 = value
|
||||
|
||||
@custom_value1.deleter
|
||||
def custom_value1(self):
|
||||
self._custom_value1 = ''
|
||||
|
||||
@property
|
||||
def custom_value2(self):
|
||||
return self._custom_value2
|
||||
|
||||
@custom_value2.setter
|
||||
def custom_value2(self, value):
|
||||
self._custom_value2 = value
|
||||
|
||||
@custom_value2.deleter
|
||||
def custom_value2(self):
|
||||
self._custom_value2 = ''
|
||||
|
||||
@property
|
||||
def custom_value3(self):
|
||||
return self._custom_value3
|
||||
|
||||
@custom_value3.setter
|
||||
def custom_value3(self, value):
|
||||
self._custom_value3 = value
|
||||
|
||||
@custom_value3.deleter
|
||||
def custom_value3(self):
|
||||
self._custom_value3 = ''
|
||||
|
||||
@property
|
||||
def custom_value4(self):
|
||||
return self._custom_value4
|
||||
|
||||
@custom_value4.setter
|
||||
def custom_value4(self, value):
|
||||
self._custom_value4 = value
|
||||
|
||||
@custom_value4.deleter
|
||||
def custom_value4(self):
|
||||
self._custom_value4 = ''
|
||||
|
||||
@property
|
||||
def ledger(self):
|
||||
return self._ledger
|
||||
|
||||
@ledger.setter
|
||||
def ledger(self, ledger: List[Dict[str, Any]]):
|
||||
self._ledger = ledger
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, client: dict):
|
||||
"""
|
||||
Create a Client instance from a JSON-like dictionary.
|
||||
"""
|
||||
contacts = [ClientContact.from_json(contact) for contact in client.get('contacts', [])]
|
||||
documents = [Document.from_json(document) for document in client.get('documents', [])]
|
||||
ledger_processed = []
|
||||
for index, item in enumerate(client.get('ledger', [])):
|
||||
if index % 2 == 0:
|
||||
# Processing ClientInvoice
|
||||
ledger_processed = ClientInvoice.from_json(item)
|
||||
|
||||
else:
|
||||
# Processing ClientPayment
|
||||
ledger_processed = ClientPayment.from_json(item)
|
||||
|
||||
|
||||
return cls(client_id=client['id'],
|
||||
name=client['name'],
|
||||
private_notes=client['private_notes'],
|
||||
public_notes=client['public_notes'],
|
||||
balance=client['balance'],
|
||||
paid_to_date=client['paid_to_date'],
|
||||
payment_balance=client['payment_balance'],
|
||||
credit_balance=client['credit_balance'],
|
||||
settings=ClientSettings.from_json(client['settings']),
|
||||
is_deleted=client['is_deleted'],
|
||||
updated_at=client['updated_at'],
|
||||
archived_at=client['archived_at'],
|
||||
created_at=client['created_at'],
|
||||
display_name=client['display_name'],
|
||||
contacts=contacts,
|
||||
documents=documents,
|
||||
address=client['address1'],
|
||||
city=client['city'],
|
||||
state=client['state'],
|
||||
postal_code=client['postal_code'],
|
||||
phone=client['phone'],
|
||||
custom_value1=client['custom_value1'],
|
||||
custom_value2=client['custom_value2'],
|
||||
custom_value3=client['custom_value3'],
|
||||
custom_value4=client['custom_value4'],
|
||||
ledger=ledger_processed)
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
Convert the Client instance to a JSON-like dictionary.
|
||||
"""
|
||||
return json.dumps({'id': self._id,
|
||||
'name': self._name,
|
||||
'private_notes': self._private_notes,
|
||||
'public_notes': self._public_notes,
|
||||
'balance': self._balance,
|
||||
'paid_to_date': self._paid_to_date,
|
||||
'payment_balance': self._payment_balance,
|
||||
'credit_balance': self._credit_balance,
|
||||
'settings': self._settings,
|
||||
'is_deleted': self._is_deleted,
|
||||
'updated_at': self._updated_at,
|
||||
'archived_at': self._archived_at,
|
||||
'created_at': self._created_at,
|
||||
'display_name': self._display_name,
|
||||
'contacts': [contact.to_json() for contact in self._contacts],
|
||||
'documents': [document.to_json() for document in self._documents],
|
||||
'address1': self._address,
|
||||
'city': self._city,
|
||||
'state': self._state,
|
||||
'postal_code': self._postal_code,
|
||||
'phone': self._phone,
|
||||
'custom_value1': self._custom_value1,
|
||||
'custom_value2': self._custom_value2,
|
||||
'custom_value3': self._custom_value3,
|
||||
'custom_value4': self._custom_value4,
|
||||
'ledger': self._ledger})
|
||||
|
||||
254
pyinvoiceninja/models/clientContact.py
Normal file
254
pyinvoiceninja/models/clientContact.py
Normal file
@ -0,0 +1,254 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
class ClientContact(object):
|
||||
def __init__(self, contact_id: str = '',
|
||||
first_name: str = '',
|
||||
last_name: str = '',
|
||||
email: str = '',
|
||||
phone: str = '',
|
||||
password: str = '',
|
||||
created_at: int = 0,
|
||||
updated_at: int = 0,
|
||||
archived_at: int = 0,
|
||||
is_primary: bool = False,
|
||||
is_locked: bool = False,
|
||||
send_email: bool = False,
|
||||
contact_key: str = '',
|
||||
last_login: int = 0,
|
||||
portal_link: str = '',
|
||||
custom_value1: str = '',
|
||||
custom_value2: str = '',
|
||||
custom_value3: str = '',
|
||||
custom_value4: str = '') -> None:
|
||||
self._contact_id = contact_id
|
||||
self._first_name = first_name
|
||||
self._last_name = last_name
|
||||
self._email = email
|
||||
self._phone = phone
|
||||
self._password = password
|
||||
self._created_at = created_at
|
||||
self._updated_at = updated_at
|
||||
self._archived_at = archived_at
|
||||
self._is_primary = is_primary
|
||||
self._is_locked = is_locked
|
||||
self._contact_key = contact_key
|
||||
self._last_login = last_login
|
||||
self._send_email = send_email
|
||||
self._portal_link = portal_link
|
||||
self._custom_value1 = custom_value1
|
||||
self._custom_value2 = custom_value2
|
||||
self._custom_value3 = custom_value3
|
||||
self._custom_value4 = custom_value4
|
||||
|
||||
@property
|
||||
def contact_id(self) -> str:
|
||||
return self._contact_id
|
||||
|
||||
@property
|
||||
def first_name(self) -> str:
|
||||
return self._first_name
|
||||
|
||||
@first_name.setter
|
||||
def first_name(self, first_name: str) -> None:
|
||||
self._first_name = first_name
|
||||
|
||||
@first_name.deleter
|
||||
def first_name(self) -> None:
|
||||
self._first_name = ''
|
||||
|
||||
@property
|
||||
def last_name(self) -> str:
|
||||
return self._last_name
|
||||
|
||||
@last_name.setter
|
||||
def last_name(self, last_name: str) -> None:
|
||||
self._last_name = last_name
|
||||
|
||||
@last_name.deleter
|
||||
def last_name(self) -> None:
|
||||
self._last_name = ''
|
||||
|
||||
@property
|
||||
def email(self) -> str:
|
||||
return self._email
|
||||
|
||||
@email.setter
|
||||
def email(self, email: str) -> None:
|
||||
self._email = email
|
||||
|
||||
@email.deleter
|
||||
def email(self) -> None:
|
||||
self._email = ''
|
||||
|
||||
@property
|
||||
def phone(self) -> str:
|
||||
return self._phone
|
||||
|
||||
@phone.setter
|
||||
def phone(self, phone: str) -> None:
|
||||
self._phone = phone
|
||||
|
||||
@phone.deleter
|
||||
def phone(self) -> None:
|
||||
self._phone = ''
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
return self._password
|
||||
|
||||
@password.setter
|
||||
def password(self, password: str) -> None:
|
||||
self._password = password
|
||||
|
||||
@password.deleter
|
||||
def password(self) -> None:
|
||||
self._password = ''
|
||||
|
||||
@property
|
||||
def created_at(self) -> int:
|
||||
return self._created_at
|
||||
|
||||
@property
|
||||
def updated_at(self) -> int:
|
||||
return self._updated_at
|
||||
|
||||
@property
|
||||
def archived_at(self) -> int:
|
||||
return self._archived_at
|
||||
|
||||
@property
|
||||
def is_primary(self) -> bool:
|
||||
return self._is_primary
|
||||
|
||||
@is_primary.setter
|
||||
def is_primary(self, is_primary: bool) -> None:
|
||||
self._is_primary = is_primary
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
return self._is_locked
|
||||
|
||||
@is_locked.setter
|
||||
def is_locked(self, is_locked: bool) -> None:
|
||||
self._is_locked = is_locked
|
||||
|
||||
@property
|
||||
def contact_key(self) -> str:
|
||||
return self._contact_key
|
||||
|
||||
@property
|
||||
def last_login(self) -> int:
|
||||
return self._last_login
|
||||
|
||||
@property
|
||||
def send_email(self) -> bool:
|
||||
return self._send_email
|
||||
|
||||
@send_email.setter
|
||||
def send_email(self, send_email: bool) -> None:
|
||||
self._send_email = send_email
|
||||
|
||||
@property
|
||||
def portal_link(self) -> str:
|
||||
return self._portal_link
|
||||
|
||||
@property
|
||||
def custom_value1(self) -> str:
|
||||
return self._custom_value1
|
||||
|
||||
@custom_value1.setter
|
||||
def custom_value1(self, value: str) -> None:
|
||||
self._custom_value1 = value
|
||||
|
||||
@custom_value1.deleter
|
||||
def custom_value1(self) -> None:
|
||||
self._custom_value1 = ''
|
||||
|
||||
@property
|
||||
def custom_value2(self) -> str:
|
||||
return self._custom_value2
|
||||
|
||||
@custom_value2.setter
|
||||
def custom_value2(self, value: str) -> None:
|
||||
self._custom_value2 = value
|
||||
|
||||
@custom_value2.deleter
|
||||
def custom_value2(self) -> None:
|
||||
self._custom_value2 = ''
|
||||
|
||||
@property
|
||||
def custom_value3(self) -> str:
|
||||
return self._custom_value3
|
||||
|
||||
@custom_value3.setter
|
||||
def custom_value3(self, value: str) -> None:
|
||||
self._custom_value3 = value
|
||||
|
||||
@custom_value3.deleter
|
||||
def custom_value3(self) -> None:
|
||||
self._custom_value3 = ''
|
||||
|
||||
@property
|
||||
def custom_value4(self) -> str:
|
||||
return self._custom_value4
|
||||
|
||||
@custom_value4.setter
|
||||
def custom_value4(self, value: str) -> None:
|
||||
self._custom_value4 = value
|
||||
|
||||
@custom_value4.deleter
|
||||
def custom_value4(self) -> None:
|
||||
self._custom_value4 = ''
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, contact: Dict[str, Any]) -> 'ClientContact':
|
||||
return cls(contact_id=contact['id'],
|
||||
first_name=contact['first_name'],
|
||||
last_name=contact['last_name'],
|
||||
email=contact['email'],
|
||||
created_at=contact['created_at'],
|
||||
updated_at=contact['updated_at'],
|
||||
archived_at=contact['archived_at'],
|
||||
is_primary=contact['is_primary'],
|
||||
is_locked=contact['is_locked'],
|
||||
phone=contact['phone'],
|
||||
custom_value1=contact['custom_value1'],
|
||||
custom_value2=contact['custom_value2'],
|
||||
custom_value3=contact['custom_value3'],
|
||||
custom_value4=contact['custom_value4'],
|
||||
contact_key=contact['contact_key'],
|
||||
send_email=contact['send_email'],
|
||||
last_login=contact['last_login'],
|
||||
password=contact['password'],
|
||||
portal_link=contact['link'])
|
||||
|
||||
@classmethod
|
||||
def from_json_list(cls, contacts: List[Dict[str, Any]]) -> List['ClientContact']:
|
||||
return [cls.from_json(contact) for contact in contacts]
|
||||
|
||||
def to_json(self) -> str:
|
||||
import json
|
||||
contact = {
|
||||
"contact_id": self._contact_id,
|
||||
"first_name": self._first_name,
|
||||
"last_name": self._last_name,
|
||||
"email": self._email,
|
||||
"phone": self._phone,
|
||||
"created_at": self._created_at,
|
||||
"updated_at": self._updated_at,
|
||||
"archived_at": self._archived_at,
|
||||
"is_primary": self._is_primary,
|
||||
"is_locked": self._is_locked,
|
||||
"send_email": self._send_email,
|
||||
"contact_key": self._contact_key,
|
||||
"last_login": self._last_login,
|
||||
"portal_link": self._portal_link,
|
||||
"custom_value1": self._custom_value1,
|
||||
"custom_value2": self._custom_value2,
|
||||
"custom_value3": self._custom_value3,
|
||||
"custom_value4": self._custom_value4
|
||||
}
|
||||
return json.dumps(contact)
|
||||
|
||||
48
pyinvoiceninja/models/clientInvoice.py
Normal file
48
pyinvoiceninja/models/clientInvoice.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
import json
|
||||
|
||||
class ClientInvoice:
|
||||
def __init__(self, invoice_id: Optional[str] = None,
|
||||
notes: Optional[str] = None,
|
||||
balance: Optional[int] = None,
|
||||
adjustment: Optional[int] = None,
|
||||
activity_id: Optional[int] = None,
|
||||
created_at: Optional[int] = None,
|
||||
updated_at: Optional[int] = None,
|
||||
archived_at: Optional[int] = None):
|
||||
self.invoice_id = invoice_id
|
||||
self.notes = notes
|
||||
self.balance = balance
|
||||
self.adjustment = adjustment
|
||||
self.activity_id = activity_id
|
||||
self.created_at = created_at
|
||||
self.updated_at = updated_at
|
||||
self.archived_at = archived_at
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type['ClientInvoice'], invoice: Dict[str, Any]) -> 'ClientInvoice':
|
||||
return cls(invoice_id=invoice.get('invoice_id', None),
|
||||
notes=invoice.get('notes', None),
|
||||
balance=invoice.get('balance', None),
|
||||
adjustment=invoice.get('adjustment', None),
|
||||
activity_id=invoice.get('activity_id', None),
|
||||
created_at=invoice.get('created_at', None),
|
||||
updated_at=invoice.get('updated_at', None),
|
||||
archived_at=invoice.get('archived_at', None))
|
||||
|
||||
def __str__(self):
|
||||
return f'Invoice: {self.invoice_id}, {self.balance}'
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps({'invoice_id': self.invoice_id,
|
||||
'notes': self.notes,
|
||||
'balance': self.balance,
|
||||
'adjustment': self.adjustment,
|
||||
'activity_id': self.activity_id,
|
||||
'created_at': self.created_at,
|
||||
'archive_at': self.archived_at})
|
||||
|
||||
48
pyinvoiceninja/models/clientPayment.py
Normal file
48
pyinvoiceninja/models/clientPayment.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
import json
|
||||
|
||||
class ClientPayment:
|
||||
def __init__(self, payment_id: Optional[str] = None,
|
||||
notes: Optional[str] = None,
|
||||
balance: Optional[int] = None,
|
||||
adjustment: Optional[int] = None,
|
||||
activity_id: Optional[int] = None,
|
||||
created_at: Optional[int] = None,
|
||||
updated_at: Optional[int] = None,
|
||||
archived_at: Optional[int] = None):
|
||||
self.payment_id = payment_id
|
||||
self.notes = notes
|
||||
self.balance = balance
|
||||
self.adjustment = adjustment
|
||||
self.activity_id = activity_id
|
||||
self.created_at = created_at
|
||||
self.updated_at = updated_at
|
||||
self.archived_at = archived_at
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type['ClientLedger'], ledger: Dict[str, Any]) -> 'ClientLedger':
|
||||
return cls(payment_id=ledger.get('payment_id', None),
|
||||
notes=ledger.get('notes', None),
|
||||
balance=ledger.get('balance', None),
|
||||
adjustment=ledger.get('adjustment', None),
|
||||
activity_id=ledger.get('activity_id', None),
|
||||
created_at=ledger.get('created_at', None),
|
||||
updated_at=ledger.get('updated_at', None),
|
||||
archived_at=ledger.get('archived_at', None))
|
||||
|
||||
def __str__(self):
|
||||
return f'Ledger: {self.payment_id}, {self.balance}'
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps({'payment_id': self.payment_id,
|
||||
'notes': self.notes,
|
||||
'balance': self.balance,
|
||||
'adjustment': self.adjustment,
|
||||
'activity_id': self.activity_id,
|
||||
'created_at': self.created_at,
|
||||
'archive_at': self.archived_at})
|
||||
|
||||
42
pyinvoiceninja/models/clientSettings.py
Normal file
42
pyinvoiceninja/models/clientSettings.py
Normal file
@ -0,0 +1,42 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
import json
|
||||
|
||||
class ClientSettings:
|
||||
def __init__(self, currency_id: Optional[str] = None,
|
||||
language_id: Optional[str] = None,
|
||||
payment_terms: Optional[int] = None,
|
||||
default_task_rate: Optional[float] = None,
|
||||
send_reminders: Optional[bool] = None,
|
||||
valid_until: Optional[str] = None,
|
||||
enable_client_portal_password: Optional[bool] = None):
|
||||
self.currency_id = currency_id
|
||||
self.language_id = language_id
|
||||
self.payment_terms = payment_terms
|
||||
self.default_task_rate = default_task_rate
|
||||
self.send_reminders = send_reminders
|
||||
self.valid_until = valid_until
|
||||
self.enable_client_portal_password = enable_client_portal_password
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type['ClientSettings'], settings: Dict[str, Any]) -> 'ClientSettings':
|
||||
return cls(currency_id=settings.get('currency_id', None),
|
||||
language_id=settings.get('language_id', None),
|
||||
payment_terms=settings.get('payment_terms', None),
|
||||
default_task_rate=settings.get('default_task_rate', None),
|
||||
send_reminders=settings.get('send_reminders', None),
|
||||
valid_until=settings.get('valid_until', None),
|
||||
enable_client_portal_password=settings.get('enable_client_portal_password', None))
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps({'currency_id': self.currency_id,
|
||||
'language_id': self.language_id,
|
||||
'payment_terms': self.payment_terms,
|
||||
'default_task_rate': self.default_task_rate,
|
||||
'send_reminders': self.send_reminders,
|
||||
'valid_until': self.valid_until,
|
||||
'enable_client_portal_password': self.enable_client_portal_password})
|
||||
|
||||
147
pyinvoiceninja/models/document.py
Normal file
147
pyinvoiceninja/models/document.py
Normal file
@ -0,0 +1,147 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
import json
|
||||
|
||||
class Document:
|
||||
def __init__(self, document_id: str = '',
|
||||
user_id: str = '',
|
||||
assigned_user_id: str = '',
|
||||
project_id: str = '',
|
||||
vendor_id: str = '',
|
||||
name: str = '',
|
||||
url: str = '',
|
||||
preview: str = '',
|
||||
document_type: str = '',
|
||||
disk: str = '',
|
||||
document_hash: str = '',
|
||||
is_deleted: bool = False,
|
||||
is_default: bool = False,
|
||||
created_at: int = 0,
|
||||
updated_at: int = 0,
|
||||
deleted_at: int = 0):
|
||||
self._document_id = document_id
|
||||
self._user_id = user_id
|
||||
self._assigned_user_id = assigned_user_id
|
||||
self._project_id = project_id
|
||||
self._vendor_id = vendor_id
|
||||
self._name = name
|
||||
self._url = url
|
||||
self._preview = preview
|
||||
self._document_type = document_type
|
||||
self._disk = disk
|
||||
self._document_hash = document_hash
|
||||
self._is_deleted = is_deleted
|
||||
self._is_default = is_default
|
||||
self._created_at = created_at
|
||||
self._updated_at = updated_at
|
||||
self._deleted_at = deleted_at
|
||||
|
||||
@property
|
||||
def document_id(self) -> str:
|
||||
return self._document_id
|
||||
|
||||
@property
|
||||
def user_id(self) -> str:
|
||||
return self._user_id
|
||||
|
||||
@property
|
||||
def assigned_user_id(self) -> str:
|
||||
return self._assigned_user_id
|
||||
|
||||
@property
|
||||
def project_id(self) -> str:
|
||||
return self._project_id
|
||||
|
||||
@property
|
||||
def vendor_id(self) -> str:
|
||||
return self._vendor_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def preview(self) -> str:
|
||||
return self._preview
|
||||
|
||||
@property
|
||||
def document_type(self) -> str:
|
||||
return self._document_type
|
||||
|
||||
@property
|
||||
def disk(self) -> str:
|
||||
return self._disk
|
||||
|
||||
@property
|
||||
def document_hash(self) -> str:
|
||||
return self._document_hash
|
||||
|
||||
@property
|
||||
def is_deleted(self) -> bool:
|
||||
return self._is_deleted
|
||||
|
||||
@property
|
||||
def is_default(self) -> bool:
|
||||
return self._is_default
|
||||
|
||||
@property
|
||||
def created_at(self) -> int:
|
||||
return self._created_at
|
||||
|
||||
@property
|
||||
def updated_at(self) -> int:
|
||||
return self._updated_at
|
||||
|
||||
@property
|
||||
def deleted_at(self) -> int:
|
||||
return self._deleted_at
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, document: Dict[str, Any]) -> 'Document':
|
||||
return cls(document_id=document.get('id', ''),
|
||||
user_id=document.get('user_id', ''),
|
||||
assigned_user_id=document.get('assigned_user_id', ''),
|
||||
project_id=document.get('project_id', ''),
|
||||
vendor_id=document.get('vendor_id', ''),
|
||||
name=document.get('name', ''),
|
||||
url=document.get('url', ''),
|
||||
preview=document.get('preview', ''),
|
||||
document_type=document.get('type', ''),
|
||||
disk=document.get('disk', ''),
|
||||
document_hash=document.get('hash', ''),
|
||||
is_deleted=document.get('is_deleted', False),
|
||||
is_default=document.get('is_default', False),
|
||||
created_at=document.get('created_at', 0),
|
||||
updated_at=document.get('updated_at', 0),
|
||||
deleted_at=document.get('deleted_at', 0))
|
||||
|
||||
@classmethod
|
||||
def from_json_list(cls, documents: List[Dict[str, Any]]) -> List['Document']:
|
||||
return [cls.from_json(document) for document in documents]
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps({'document_id': self._document_id,
|
||||
'user_id': self._user_id,
|
||||
'assigned_user_id': self._assigned_user_id,
|
||||
'project_id': self._project_id,
|
||||
'vendor_id': self._vendor_id,
|
||||
'name': self._name,
|
||||
'url': self._url,
|
||||
'preview': self._preview,
|
||||
'document_type': self._document_type,
|
||||
'disk': self._disk,
|
||||
'document_hash': self._document_hash,
|
||||
'is_deleted': self._is_deleted,
|
||||
'is_default': self._is_default,
|
||||
'created_at': self._created_at,
|
||||
'updated_at': self._updated_at,
|
||||
'deleted_at': self._deleted_at})
|
||||
|
||||
def __str__(self):
|
||||
return f'Document({self._document_id}, {self._name})'
|
||||
|
||||
14
shell.nix
14
shell.nix
@ -1,12 +1,24 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") {} }:
|
||||
|
||||
|
||||
with pkgs;
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = [
|
||||
inlyne # markdown viewer
|
||||
|
||||
# Python development environment
|
||||
(python3.withPackages(ps: with ps; [
|
||||
# For SDK configuration
|
||||
python-dotenv
|
||||
|
||||
# Logging
|
||||
loguru
|
||||
|
||||
# HTTP client
|
||||
requests
|
||||
|
||||
# Testing
|
||||
pytest
|
||||
]))
|
||||
];
|
||||
}
|
||||
|
||||
14
tests/conftest.py
Normal file
14
tests/conftest.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Library imports
|
||||
import pytest
|
||||
|
||||
####
|
||||
# Global fixtures
|
||||
####
|
||||
|
||||
# App client for testing
|
||||
@pytest.fixture(scope='module')
|
||||
def client():
|
||||
app = create_app(config_class='tests/testing_config.py')
|
||||
with app.app_context():
|
||||
yield app
|
||||
|
||||
6
tests/test.env
Normal file
6
tests/test.env
Normal file
@ -0,0 +1,6 @@
|
||||
# Base URL of an Invoice Ninja instance
|
||||
BASE_URL=
|
||||
|
||||
# API token for an Invoice Ninja instance
|
||||
API_TOKEN=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user