refactored to use an ApiClient class
This commit is contained in:
32
README.md
32
README.md
@@ -14,7 +14,7 @@ See `Initializing` for additional setup.
|
||||
# The Goodies
|
||||
|
||||
## Invoices
|
||||
`get`, `get_one`, `create`, `send`, `update` and `delete` are the bread and butter functions here.
|
||||
`client.get_one_invoice`, `client.create_invoice`, `client.send_invoice`, `client.update_invoice` and `client.delete_invoice` are the bread and butter methods here.
|
||||
|
||||
The `get...` functions return some handy `NamedTuple` instances with helpful attributes, notably `FreshbooksInvoice.lines` which have `Decimal` values where you would hope to find them. Also some lookups for addressing the `FreshbooksLine`s you may be interested in.
|
||||
|
||||
@@ -50,13 +50,14 @@ class FreshbooksLine(NamedTuple):
|
||||
amount: Decimal
|
||||
```
|
||||
|
||||
Then you have helpers `get_all_draft_invoices`, `get_all_invoices_for_org_name`, `get_all_invoices_for_client_id`, and `get_draft_invoices_for_client_id`.
|
||||
Then you have helpers `client.get_all_draft_invoices`, `client.get_all_invoices_for_org_name`, `client.get_all_invoices_for_client_id`, and `client.get_draft_invoices_for_client_id`.
|
||||
|
||||
### Create an Invoice
|
||||
The signature of `invoice.create` is like so:
|
||||
The signature of `client.create_invoice` is like so:
|
||||
|
||||
```python
|
||||
def create(
|
||||
self,
|
||||
client_id: int,
|
||||
notes: str,
|
||||
lines: list[dict],
|
||||
@@ -78,7 +79,7 @@ Status can be any of the `v3_status` values as a `str` or `1` or `4` (draft/paid
|
||||
|
||||
|
||||
## Clients
|
||||
`get_all_clients`, `get_one`, `create`, and `delete` are available here.
|
||||
`client.get_all_clients`, `client.create_client`, and `client.delete_client` are available here.
|
||||
|
||||
Once more the `get...` functions return `NamedTuple` instances with some helpful attributes, notably `FreshbooksClient.contacts` and a couple of related lookups (`.contact_id_email_lookup` and `.email_contact_id_lookup`).
|
||||
|
||||
@@ -101,21 +102,18 @@ class FreshbooksContact(NamedTuple):
|
||||
email: str
|
||||
```
|
||||
|
||||
Then, `update_contacts`, `delete_contact`, `add_contacts`, `get_freshbooks_client_from_client_id`, `get_freshbooks_client_from_email`, and `get_freshbooks_client_from_org_name`.
|
||||
Then, `client.update_contacts`, `client.delete_contact`, `client.add_contacts`, `client.get_freshbooks_client_from_client_id`, `client.get_freshbooks_client_from_email`, and `client.get_freshbooks_client_from_org_name`.
|
||||
|
||||
# Prerequisites/Configuration
|
||||
Make yourself a nice little `.env` file in your home directory or wherever you're going to be calling this library's code. It needs to contain:
|
||||
Instantiate the avt_fresh `Client` like so:
|
||||
|
||||
```bash
|
||||
FRESHBOOKS_API_CLIENT_ID='blah'
|
||||
FRESHBOOKS_API_CLIENT_SECRET='blah'
|
||||
FRESHBOOKS_REDIRECT_URI="https://blah.com/blah"
|
||||
FRESHBOOKS_ACCOUNT_ID="blah"
|
||||
```python
|
||||
client = Client(client_secret="...", client_id="...", redirect_uri="https://...", account_id="...")
|
||||
```
|
||||
|
||||
You can get and set these goodies [here](https://my.freshbooks.com/#/developer). Well, all of them except `FRESHBOOKS_ACCOUNT_ID`, which you can see (there's got to be another way??) by clicking on one of your invoices and grabbing the substring here: `https://my.freshbooks.com/#/invoice/<THIS THING>-1234567`.
|
||||
|
||||
Don't tell anyone but `FRESHBOOKS_REDIRECT_URI` can be pretty much anything! See `Initializing` below 👇.
|
||||
Don't tell anyone but `redirect_uri` can be pretty much anything! See `Initializing` below 👇.
|
||||
|
||||
## Security
|
||||
Which brings me to an important point. Currently it's going to save your OAuth tokens in the ever-so-insecure path of `~/freshbooks_oauth_token.json`. TODO: don't do this anymore. ¯\_(ツ)_/¯
|
||||
@@ -132,7 +130,7 @@ If you don't have an app there, create a really basic one. Name and description
|
||||
Application Type: "Private App"
|
||||
Scopes: `admin:all:legacy`
|
||||
|
||||
Add a redirect URI, it can actually be pretty much anything.
|
||||
Add a redirect URI, it can actually be pretty much anything. Well, preferably a URL you control since it will receive OAuth tokens.
|
||||
|
||||
Finally, once you have an app on that developer page, click into it and click "go to authentication" page. Freshbooks will pop open a tab and go to your redirect URI, appending `?code=blahblahblah` to it. Grab the "blah blah blah" value and paste it into the prompt.
|
||||
|
||||
@@ -141,14 +139,12 @@ You should only have to do this once in each environment you use this library in
|
||||
# Hardcoded Stuff / TODOs
|
||||
Here are some quirks and TODOs. PRs are welcome!:
|
||||
|
||||
Currently all invoices will have Stripe added as a payment option. There's no option to skip this, and there's no other payment methods available in this library.
|
||||
Only Python 3.10 is supported at the moment.
|
||||
|
||||
Also, only Python 3.10 is supported at the moment.
|
||||
|
||||
Then, when it comes to invoice statuses, we're only using `v3_status` strings, not the numbers. What's more, when you create an invoice we're only supporting two possible statuses: "draft" and "paid".
|
||||
When it comes to invoice statuses, we're only using `v3_status` strings, not the numbers. What's more, when you create an invoice we're only supporting two possible statuses: "draft" and "paid".
|
||||
|
||||
The `create`, `update`, and `delete` functions return dictionaries rather than an instance of the appropriate `NamedTuple`. This would be a great improvement!
|
||||
|
||||
The docs need improvement for sure: For now, have a peek at the source code, which includes pretty comprehensive type hints at the very least.
|
||||
|
||||
There are no tests! Is the other thing. Although this code has been used in production in at least one company with some success.
|
||||
There are no tests! However, this code has been used in production in at least one company with some success.
|
||||
|
||||
178
avt_fresh/api.py
Normal file
178
avt_fresh/api.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from avt_fresh import BASE_URL, REQUEST as _REQUEST
|
||||
from avt_fresh.client import (
|
||||
FreshbooksClient,
|
||||
get_freshbooks_client_from_email,
|
||||
get_freshbooks_client_from_client_id,
|
||||
get_freshbooks_client_from_org_name,
|
||||
get_all_clients,
|
||||
delete as delete_client,
|
||||
create as create_client,
|
||||
delete_contact,
|
||||
add_contacts,
|
||||
)
|
||||
from avt_fresh.invoice import (
|
||||
FreshbooksInvoice,
|
||||
get_one as get_one_invoice,
|
||||
get_all_draft_invoices,
|
||||
get_all_invoices_for_client_id,
|
||||
get_all_invoices_for_org_name,
|
||||
get_draft_invoices_for_client_id,
|
||||
create as create_invoice,
|
||||
update as update_invoice,
|
||||
delete as delete_invoice,
|
||||
send as send_invoice,
|
||||
)
|
||||
from avt_fresh.payments import (
|
||||
get_default_payment_options,
|
||||
add_payment_option_to_invoice,
|
||||
)
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(
|
||||
self, client_secret: str, client_id: str, redirect_uri: str, account_id: str
|
||||
):
|
||||
self.client_secret = client_secret
|
||||
self.client_id = client_id
|
||||
self.redirect_uri = redirect_uri
|
||||
self.account_id = account_id
|
||||
self.url_lookup = self._make_url_lookup(account_id)
|
||||
|
||||
@staticmethod
|
||||
def _make_url_lookup(account_id: str) -> dict[str, str]:
|
||||
return {
|
||||
"client": f"{BASE_URL}/accounting/account/{account_id}/users/clients",
|
||||
"invoice": f"{BASE_URL}/accounting/account/{account_id}/invoices/invoices",
|
||||
"payments": f"{BASE_URL}/payments/account/{account_id}",
|
||||
}
|
||||
|
||||
def _REQUEST(
|
||||
self, *, what: str, method_name: str, endpoint: str, stuff: dict | None = None
|
||||
) -> dict:
|
||||
if method_name not in ("GET", "PUT", "POST"):
|
||||
raise Exception
|
||||
if what == "payments" and method_name == "PUT":
|
||||
raise Exception
|
||||
|
||||
if what == "client" and method_name == "PUT":
|
||||
url = f"{BASE_URL}/accounting/account"
|
||||
else:
|
||||
url = self.url_lookup[what]
|
||||
return _REQUEST(
|
||||
url=url, method_name=method_name, endpoint=endpoint, stuff=stuff
|
||||
)
|
||||
|
||||
def _GET(self, *, what: str, endpoint: str, params=None):
|
||||
return self._REQUEST(
|
||||
what=what, method_name="GET", endpoint=endpoint, stuff=params
|
||||
)
|
||||
|
||||
def _POST(self, *, what: str, endpoint: str, data: dict):
|
||||
return self._REQUEST(
|
||||
what=what, method_name="POST", endpoint=endpoint, stuff=data
|
||||
)
|
||||
|
||||
def _PUT(self, *, what: str, thing_id: int, data: dict):
|
||||
return self._REQUEST(
|
||||
what=what, method_name="PUT", endpoint=f"/{thing_id}", stuff=data
|
||||
)
|
||||
|
||||
def get_one_invoice(self, invoice_id: int) -> FreshbooksInvoice:
|
||||
return get_one_invoice(get_func=self._GET, invoice_id=invoice_id)
|
||||
|
||||
def get_all_draft_invoices(self) -> list[FreshbooksInvoice]:
|
||||
return get_all_draft_invoices(get_func=self._GET)
|
||||
|
||||
def get_all_invoices_for_org_name(self, org_name: str) -> list[FreshbooksInvoice]:
|
||||
return get_all_invoices_for_org_name(get_func=self._GET, org_name=org_name)
|
||||
|
||||
def get_all_invoices_for_client_id(self, client_id: int) -> list[FreshbooksInvoice]:
|
||||
return get_all_invoices_for_client_id(get_func=self._GET, client_id=client_id)
|
||||
|
||||
def get_draft_invoices_for_client_id(
|
||||
self, client_id: int
|
||||
) -> list[FreshbooksInvoice]:
|
||||
return get_draft_invoices_for_client_id(get_func=self._GET, client_id=client_id)
|
||||
|
||||
def create_invoice(
|
||||
self,
|
||||
*,
|
||||
client_id: int,
|
||||
notes: str,
|
||||
lines: list[dict],
|
||||
status: str | int,
|
||||
contacts: list[dict] | None = None,
|
||||
po_number=None,
|
||||
create_date=None,
|
||||
) -> dict:
|
||||
return create_invoice(
|
||||
post_func=self._POST,
|
||||
client_id=client_id,
|
||||
notes=notes,
|
||||
lines=lines,
|
||||
status=status,
|
||||
contacts=contacts,
|
||||
po_number=po_number,
|
||||
create_date=create_date,
|
||||
)
|
||||
|
||||
def update_invoice(self, invoice_id: int, **kwargs) -> dict:
|
||||
return update_invoice(put_func=self._PUT, invoice_id=invoice_id, **kwargs)
|
||||
|
||||
def delete_invoice(self, invoice_id: int) -> dict:
|
||||
return delete_invoice(put_func=self._PUT, invoice_id=invoice_id)
|
||||
|
||||
def send_invoice(self, invoice_id: int) -> dict:
|
||||
return send_invoice(put_func=self._PUT, invoice_id=invoice_id)
|
||||
|
||||
def get_freshbooks_client_from_email(self, email: str) -> FreshbooksClient:
|
||||
return get_freshbooks_client_from_email(get_func=self._GET, email=email)
|
||||
|
||||
def get_freshbooks_client_from_client_id(self, client_id: int) -> FreshbooksClient:
|
||||
return get_freshbooks_client_from_client_id(
|
||||
get_func=self._GET, client_id=client_id
|
||||
)
|
||||
|
||||
def get_freshbooks_client_from_org_name(self, org_name: str) -> FreshbooksClient:
|
||||
return get_freshbooks_client_from_org_name(
|
||||
get_func=self._GET, org_name=org_name
|
||||
)
|
||||
|
||||
def get_all_clients(self) -> list[FreshbooksClient]:
|
||||
return get_all_clients(get_func=self._GET)
|
||||
|
||||
def create_client(
|
||||
self, first_name: str, last_name: str, email: str, organization: str
|
||||
) -> FreshbooksClient:
|
||||
return create_client(
|
||||
get_func=self._GET,
|
||||
post_func=self._POST,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
organization=organization,
|
||||
)
|
||||
|
||||
def delete_client(self, client_id: int) -> None:
|
||||
delete_client(put_func=self._PUT, client_id=client_id)
|
||||
|
||||
def add_contacts(self, client_id: int, contacts: list[dict]) -> None:
|
||||
add_contacts(
|
||||
get_func=self._GET,
|
||||
put_func=self._PUT,
|
||||
client_id=client_id,
|
||||
contacts=contacts,
|
||||
)
|
||||
|
||||
def delete_contact(self, client_id: int, email: str) -> None:
|
||||
delete_contact(
|
||||
get_func=self._GET, put_func=self._PUT, client_id=client_id, email=email
|
||||
)
|
||||
|
||||
def get_default_payment_options(self) -> dict:
|
||||
return get_default_payment_options(get_func=self._GET)
|
||||
|
||||
def add_payment_option_to_invoice(self, invoice_id: int, gateway_name: str) -> dict:
|
||||
return add_payment_option_to_invoice(
|
||||
post_func=self._POST, invoice_id=invoice_id, gateway_name=gateway_name
|
||||
)
|
||||
@@ -1,12 +1,9 @@
|
||||
from functools import partial
|
||||
from typing import NamedTuple
|
||||
import typing
|
||||
|
||||
import requests
|
||||
|
||||
from avt_fresh import BASE_URL, REQUEST as __REQUEST, ACCOUNT_ID, make_headers
|
||||
WHAT = "client"
|
||||
|
||||
|
||||
class FreshbooksContact(NamedTuple):
|
||||
class FreshbooksContact(typing.NamedTuple):
|
||||
contact_id: int
|
||||
first_name: str
|
||||
last_name: str
|
||||
@@ -25,7 +22,7 @@ class FreshbooksContact(NamedTuple):
|
||||
}
|
||||
|
||||
|
||||
class FreshbooksClient(NamedTuple):
|
||||
class FreshbooksClient(typing.NamedTuple):
|
||||
client_id: int
|
||||
email: str
|
||||
organization: str
|
||||
@@ -57,22 +54,6 @@ class FreshbooksClient(NamedTuple):
|
||||
)
|
||||
|
||||
|
||||
URL = f"{BASE_URL}/accounting/account/{ACCOUNT_ID}/users/clients"
|
||||
_REQUEST = partial(__REQUEST, url=URL)
|
||||
|
||||
|
||||
def _GET(endpoint, params=None):
|
||||
return _REQUEST(method_name="GET", endpoint=endpoint, stuff=params)
|
||||
|
||||
|
||||
def _POST(endpoint, data: dict):
|
||||
return _REQUEST(method_name="POST", endpoint=endpoint, stuff=data)
|
||||
|
||||
|
||||
def _PUT(endpoint, thing_id: int, data: dict):
|
||||
return _REQUEST(method_name="PUT", endpoint=f"{endpoint}/{thing_id}", stuff=data)
|
||||
|
||||
|
||||
class NoResult(Exception):
|
||||
pass
|
||||
|
||||
@@ -84,59 +65,74 @@ class MoreThanOne(Exception):
|
||||
INCLUDE = "include[]=contacts"
|
||||
|
||||
|
||||
def get_freshbooks_client_from_email(email: str) -> FreshbooksClient:
|
||||
def get_freshbooks_client_from_email(
|
||||
*, get_func: typing.Callable, email: str
|
||||
) -> FreshbooksClient:
|
||||
try:
|
||||
return get_one(_GET(f"?search[email]={email}&{INCLUDE}"))
|
||||
return _get_one(
|
||||
get_func(what=WHAT, endpoint=f"?search[email]={email}&{INCLUDE}")
|
||||
)
|
||||
except NoResult as e:
|
||||
raise NoResult(email) from e
|
||||
|
||||
|
||||
def get_freshbooks_client_from_org_name(org_name: str) -> FreshbooksClient:
|
||||
return get_one(_GET(f"?search[organization_like]={org_name}&{INCLUDE}"))
|
||||
|
||||
|
||||
def get_freshbooks_client_from_client_id(client_id: int) -> FreshbooksClient:
|
||||
return FreshbooksClient.from_api(**_GET(f"{client_id}?{INCLUDE}")["client"])
|
||||
|
||||
|
||||
def get_one(response: dict) -> FreshbooksClient:
|
||||
clients = [FreshbooksClient.from_api(**client) for client in response["clients"]]
|
||||
if len(clients) > 1:
|
||||
print("warning, more than one result, returning the first")
|
||||
elif not clients:
|
||||
raise NoResult
|
||||
return clients[0]
|
||||
|
||||
|
||||
def get_all_clients() -> list[FreshbooksClient]:
|
||||
return _GET(f"?{INCLUDE}")
|
||||
|
||||
|
||||
def delete(client_id) -> None:
|
||||
requests.put(
|
||||
f"{BASE_URL}/accounting/account/{client_id}",
|
||||
json={"client": {"vis_state": 1}},
|
||||
headers=make_headers(),
|
||||
def get_freshbooks_client_from_org_name(
|
||||
*, get_func: typing.Callable, org_name: str
|
||||
) -> FreshbooksClient:
|
||||
return _get_one(
|
||||
get_func(what=WHAT, endpoint=f"?search[organization_like]={org_name}&{INCLUDE}")
|
||||
)
|
||||
|
||||
|
||||
def get_freshbooks_client_from_client_id(
|
||||
*, get_func: typing.Callable, client_id: int
|
||||
) -> FreshbooksClient:
|
||||
return FreshbooksClient.from_api(
|
||||
**get_func(what=WHAT, endpoint=f"{client_id}?{INCLUDE}")["client"]
|
||||
)
|
||||
|
||||
|
||||
def get_all_clients(*, get_func: typing.Callable) -> list[FreshbooksClient]:
|
||||
response = get_func(what=WHAT, endpoint="")
|
||||
num_results = response["total"]
|
||||
return [FreshbooksClient.from_api(**c) for c in get_func(what=WHAT, endpoint=f"?{INCLUDE}&per_page={num_results}")["clients"]]
|
||||
|
||||
|
||||
def delete(*, put_func: typing.Callable, client_id: int) -> None:
|
||||
return put_func(what=WHAT, thing_id=client_id, data={"client": {"vis_state": 1}})
|
||||
|
||||
|
||||
def create(
|
||||
first_name: str, last_name: str, email: str, organization: str
|
||||
*,
|
||||
get_func: typing.Callable,
|
||||
post_func: typing.Callable,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
organization: str,
|
||||
) -> FreshbooksClient:
|
||||
data = {
|
||||
"client": dict(
|
||||
fname=first_name, lname=last_name, email=email, organization=organization
|
||||
)
|
||||
}
|
||||
client_id = _POST(endpoint="", data=data)["client"]["id"]
|
||||
return get_freshbooks_client_from_client_id(client_id)
|
||||
client_id = post_func(what=WHAT, endpoint="", data=data)["client"]["id"]
|
||||
return get_freshbooks_client_from_client_id(get_func=get_func, client_id=client_id)
|
||||
|
||||
|
||||
def add_contacts(client_id, contacts: list[dict]) -> None:
|
||||
def add_contacts(
|
||||
*,
|
||||
get_func: typing.Callable,
|
||||
put_func: typing.Callable,
|
||||
client_id: int,
|
||||
contacts: list[dict],
|
||||
) -> None:
|
||||
"""contacts: [dict(email, fname, lname)]"""
|
||||
to_update = []
|
||||
new_contacts_email_dict = {c["email"]: c for c in contacts}
|
||||
current_contacts = get_freshbooks_client_from_client_id(client_id).contacts
|
||||
current_contacts = get_freshbooks_client_from_client_id(
|
||||
get_func=get_func, client_id=client_id
|
||||
).contacts
|
||||
|
||||
if current_contacts:
|
||||
for email, current_contact in current_contacts.items():
|
||||
@@ -148,16 +144,21 @@ def add_contacts(client_id, contacts: list[dict]) -> None:
|
||||
del new_contacts_email_dict[new_contact["email"]]
|
||||
|
||||
to_update += list(new_contacts_email_dict.values())
|
||||
update_contacts(client_id, to_update)
|
||||
_update_contacts(put_func=put_func, client_id=client_id, contacts=to_update)
|
||||
|
||||
|
||||
def delete_contact(client_id, email) -> None:
|
||||
client = get_freshbooks_client_from_client_id(client_id)
|
||||
def delete_contact(
|
||||
*, get_func: typing.Callable, put_func: typing.Callable, client_id: int, email: str
|
||||
) -> None:
|
||||
client = get_freshbooks_client_from_client_id(
|
||||
get_func=get_func, client_id=client_id
|
||||
)
|
||||
contact_to_delete = client.contacts.get(email)
|
||||
if contact_to_delete is not None:
|
||||
return update_contacts(
|
||||
client_id,
|
||||
[
|
||||
return _update_contacts(
|
||||
put_func=put_func,
|
||||
client_id=client_id,
|
||||
contacts=[
|
||||
client.dict
|
||||
for client in client.contacts.values()
|
||||
if client != contact_to_delete
|
||||
@@ -165,13 +166,24 @@ def delete_contact(client_id, email) -> None:
|
||||
)
|
||||
|
||||
|
||||
def update_contacts(client_id, contacts: list[dict]) -> None:
|
||||
_update_freshbooks_client(client_id, {"contacts": contacts})
|
||||
|
||||
|
||||
def _update_freshbooks_client(client_id, data: dict) -> None:
|
||||
requests.put(
|
||||
f"{BASE_URL}/accounting/account/{client_id}",
|
||||
json={"client": data},
|
||||
headers=make_headers(),
|
||||
def _update_contacts(
|
||||
*, put_func: typing.Callable, client_id: int, contacts: list[dict]
|
||||
) -> None:
|
||||
_update_freshbooks_client(
|
||||
put_func=put_func, client_id=client_id, data={"contacts": contacts}
|
||||
)
|
||||
|
||||
|
||||
def _update_freshbooks_client(
|
||||
*, put_func: typing.Callable, client_id: int, data: dict
|
||||
) -> None:
|
||||
put_func(what=WHAT, thing_id=client_id, data={"client": data})
|
||||
|
||||
|
||||
def _get_one(response: dict) -> FreshbooksClient:
|
||||
clients = [FreshbooksClient.from_api(**client) for client in response["clients"]]
|
||||
if len(clients) > 1:
|
||||
print("warning, more than one result, returning the first")
|
||||
elif not clients:
|
||||
raise NoResult
|
||||
return clients[0]
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import datetime as dt
|
||||
from decimal import Decimal
|
||||
from functools import partial, lru_cache
|
||||
from typing import NamedTuple
|
||||
import typing
|
||||
|
||||
from avt_fresh import BASE_URL, ACCOUNT_ID, REQUEST as __REQUEST
|
||||
|
||||
URL = f"{BASE_URL}/accounting/account/{ACCOUNT_ID}/invoices/invoices"
|
||||
_REQUEST = partial(__REQUEST, url=URL)
|
||||
WHAT = "invoice"
|
||||
|
||||
|
||||
class ArgumentError(Exception):
|
||||
@@ -25,19 +22,7 @@ class InvalidField(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _GET(endpoint, params=None):
|
||||
return _REQUEST(method_name="GET", endpoint=endpoint, stuff=params)
|
||||
|
||||
|
||||
def _POST(endpoint, data: dict):
|
||||
return _REQUEST(method_name="POST", endpoint=endpoint, stuff=data)
|
||||
|
||||
|
||||
def _PUT(endpoint, thing_id: int, data: dict):
|
||||
return _REQUEST(method_name="PUT", endpoint=f"{endpoint}/{thing_id}", stuff=data)
|
||||
|
||||
|
||||
class FreshbooksLine(NamedTuple):
|
||||
class FreshbooksLine(typing.NamedTuple):
|
||||
invoice_id: int
|
||||
client_id: int
|
||||
description: str
|
||||
@@ -70,7 +55,7 @@ class FreshbooksLine(NamedTuple):
|
||||
}
|
||||
|
||||
|
||||
class FreshbooksInvoice(NamedTuple):
|
||||
class FreshbooksInvoice(typing.NamedTuple):
|
||||
lines: list[FreshbooksLine]
|
||||
notes: str
|
||||
client_id: int
|
||||
@@ -123,25 +108,31 @@ class FreshbooksInvoice(NamedTuple):
|
||||
)
|
||||
|
||||
|
||||
def get_all_draft_invoices():
|
||||
return get(status="draft")
|
||||
def get_all_draft_invoices(*, get_func: typing.Callable) -> list[FreshbooksInvoice]:
|
||||
return _get(get_func=get_func, status="draft")
|
||||
|
||||
|
||||
def get_all_invoices_for_org_name(org_name: str) -> list[FreshbooksInvoice]:
|
||||
return get(org_name=org_name)
|
||||
def get_all_invoices_for_org_name(
|
||||
*, get_func: typing.Callable, org_name: str
|
||||
) -> list[FreshbooksInvoice]:
|
||||
return _get(get_func=get_func, org_name=org_name)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_all_invoices_for_client_id(client_id: int) -> list[FreshbooksInvoice]:
|
||||
return get(client_id=client_id)
|
||||
def get_all_invoices_for_client_id(
|
||||
*, get_func: typing.Callable, client_id: int
|
||||
) -> list[FreshbooksInvoice]:
|
||||
return _get(get_func=get_func, client_id=client_id)
|
||||
|
||||
|
||||
def get_draft_invoices_for_client_id(client_id: int) -> list[FreshbooksInvoice]:
|
||||
return get(client_id=client_id, status="draft")
|
||||
def get_draft_invoices_for_client_id(
|
||||
*, get_func: typing.Callable, client_id: int
|
||||
) -> list[FreshbooksInvoice]:
|
||||
return _get(get_func=get_func, client_id=client_id, status="draft")
|
||||
|
||||
|
||||
def get_one(invoice_id) -> FreshbooksInvoice:
|
||||
invoices = get(invoice_id)
|
||||
def get_one(*, get_func: typing.Callable, invoice_id: int) -> FreshbooksInvoice:
|
||||
invoices = _get(get_func=get_func, invoice_id=invoice_id)
|
||||
if invoices:
|
||||
if len(invoices) > 1:
|
||||
raise MoreThanOne
|
||||
@@ -149,12 +140,14 @@ def get_one(invoice_id) -> FreshbooksInvoice:
|
||||
raise DoesntExist
|
||||
|
||||
|
||||
def get(
|
||||
def _get(
|
||||
get_func: typing.Callable,
|
||||
invoice_id=None,
|
||||
client_id=None,
|
||||
org_name=None,
|
||||
status=None,
|
||||
) -> list[FreshbooksInvoice]:
|
||||
get_func = partial(get_func, what=WHAT)
|
||||
|
||||
if client_id is not None and org_name is not None:
|
||||
raise ArgumentError("Please provide either client_id or org_name")
|
||||
@@ -180,7 +173,7 @@ def get(
|
||||
full_url += f"{sep}search[v3_status]={status}"
|
||||
sep = "&"
|
||||
|
||||
response = _GET(full_url)
|
||||
response = get_func(endpoint=full_url)
|
||||
try:
|
||||
num_results = response["total"]
|
||||
except KeyError:
|
||||
@@ -195,7 +188,7 @@ def get(
|
||||
f"&include[]=lines&include[]=contacts&include[]=allowed_gateways"
|
||||
)
|
||||
|
||||
result = _GET(full_url)
|
||||
result = get_func(endpoint=full_url)
|
||||
if "invoice" in result:
|
||||
return [FreshbooksInvoice.from_api(**result["invoice"])]
|
||||
invoices = result["invoices"]
|
||||
@@ -221,6 +214,8 @@ STATUS_STRING_INT_LOOKUP = {
|
||||
|
||||
|
||||
def create(
|
||||
*,
|
||||
post_func: typing.Callable,
|
||||
client_id: int,
|
||||
notes: str,
|
||||
lines: list[dict],
|
||||
@@ -256,16 +251,20 @@ def create(
|
||||
if po_number:
|
||||
data["invoice"]["po_number"] = po_number
|
||||
|
||||
return _POST("", data=data)
|
||||
return post_func(what=WHAT, endpoint="", data=data)
|
||||
|
||||
|
||||
def update(invoice_id, **kwargs) -> dict:
|
||||
return _PUT("", invoice_id, dict(invoice=kwargs))
|
||||
def update(*, put_func: typing.Callable, invoice_id, **kwargs) -> dict:
|
||||
return put_func(what=WHAT, thing_id=invoice_id, data={"invoice": kwargs})
|
||||
|
||||
|
||||
def delete(invoice_id) -> dict:
|
||||
return _PUT("", invoice_id, dict(invoice=dict(vis_state=1)))
|
||||
def delete(*, put_func: typing.Callable, invoice_id: int) -> dict:
|
||||
return put_func(what=WHAT, thing_id=invoice_id, data={"invoice": {"vis_state": 1}})
|
||||
|
||||
|
||||
def send(invoice_id) -> dict:
|
||||
return _PUT("", invoice_id, {"invoice": {"action_email": True}})
|
||||
def send(*, put_func: typing.Callable, invoice_id: int) -> dict:
|
||||
return put_func(
|
||||
what=WHAT,
|
||||
thing_id=invoice_id,
|
||||
data={"invoice": {"action_email": True}},
|
||||
)
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
from functools import partial
|
||||
import typing
|
||||
|
||||
from . import BASE_URL, ACCOUNT_ID, REQUEST as __REQUEST
|
||||
|
||||
URL = f"{BASE_URL}/payments/account/{ACCOUNT_ID}"
|
||||
_REQUEST = partial(__REQUEST, url=URL)
|
||||
WHAT = "payments"
|
||||
|
||||
|
||||
def _GET(endpoint, params=None):
|
||||
return _REQUEST(method_name="GET", endpoint=endpoint, stuff=params)
|
||||
def get_default_payment_options(*, get_func: typing.Callable) -> dict:
|
||||
return get_func(what=WHAT, endpoint="payment_options?entity_type=invoice")
|
||||
|
||||
|
||||
def _POST(endpoint, data: dict):
|
||||
return _REQUEST(method_name="POST", endpoint=endpoint, stuff=data)
|
||||
|
||||
|
||||
def get_default_payment_options():
|
||||
return _GET("payment_options?entity_type=invoice")
|
||||
|
||||
|
||||
def add_payment_option_to_invoice(invoice_id, gateway_name="stripe"):
|
||||
return _POST(
|
||||
f"invoice/{invoice_id}/payment_options",
|
||||
def add_payment_option_to_invoice(
|
||||
post_func: typing.Callable, invoice_id: int, gateway_name: str = "stripe"
|
||||
) -> dict:
|
||||
return post_func(
|
||||
what=WHAT,
|
||||
endpoint=f"invoice/{invoice_id}/payment_options",
|
||||
data={
|
||||
"gateway_name": gateway_name,
|
||||
"entity_id": invoice_id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
certifi==2021.10.8
|
||||
charset-normalizer==2.0.12
|
||||
idna==3.3
|
||||
python-dotenv==0.20.0
|
||||
requests==2.27.1
|
||||
urllib3==1.26.9
|
||||
|
||||
9
setup.py
9
setup.py
@@ -4,14 +4,13 @@ setup(
|
||||
name="avt_fresh",
|
||||
author="Zev Averbach",
|
||||
author_email="zev@averba.ch",
|
||||
version="0.0.9",
|
||||
version="0.0.11",
|
||||
license="MIT",
|
||||
python_requires=">3.10.0",
|
||||
keywords='freshbooks API',
|
||||
url='https://github.com/zevaverbach/avt-fresh',
|
||||
keywords="freshbooks API",
|
||||
url="https://github.com/zevaverbach/avt-fresh",
|
||||
install_requires=[
|
||||
'requests',
|
||||
'python-dotenv',
|
||||
"requests",
|
||||
],
|
||||
packages=[
|
||||
"avt_fresh",
|
||||
|
||||
Reference in New Issue
Block a user