Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #339 and add support for object specific filtering #341

Merged
merged 8 commits into from
Sep 30, 2023

Conversation

bpotard
Copy link
Contributor

@bpotard bpotard commented Sep 14, 2023

Hello,

This PR adds support for all "optimised" object-specific filters supported by the xero accounting API (as documented on https://developer.xero.com/documentation/api/accounting/overview) as of August 2023.

For example:

>>> xero.contacts.filter(searchTerm='Bob',summaryOnly=True)
>>> xero.invoices.filter(Statuses=['PAID', 'VOIDED'])
>>> xero.purchaseorders.filter(DateFrom=datetime.date(2023,9,14))

It also add some typing and basic input validation on filter fields, e.g.

>>> xero.invoices.filter(IDs=['1', '2'])
...
ValueError: badly formed hexadecimal UUID string

Finally, it also resolves issue #339.

@@ -8,7 +8,7 @@ def __init__(self, name, credentials, unit_price_4dps=False, user_agent=None):
from xero import __version__ as VERSION # noqa

self.credentials = credentials
self.name = name.capitalize()
self.name = name.capitalize() if name.islower() else name
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had added this as a hack to make the tests behave "as expected". But unfortunately that breaks the behaviour for all objects that have more than one upper case letter, e.g. "PurchaseOrders", as it enforce only the first letter is capitalized.

Now that I think about it, it should really be the tests that should be updated to use the CapitalizedNames of the objects rather than a lower case version...

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The problem is with the test, not the manager.

@@ -226,13 +226,18 @@ def test_filter_ids(self):
manager = Manager("contacts", credentials)

uri, params, method, body, headers, singleobject = manager._filter(
IDs=["1", "2", "3", "4", "5"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are actually invalid IDs, as only UUIDs are accepted. Sending this as input to the xero API results in a rather strange error message in an undocumented format. I suggest to run the tests on valid input, i.e. UUIDs.

self.problem = self.errors[0]
super().__init__(response, payload["oauth_problem_advice"][0])

if payload:
Copy link
Contributor Author

@bpotard bpotard Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handles the undocumented format of error messages from xero when sending malformed UUIDs for a list filter request.

Comment on lines 44 to 72
OBJECT_FILTER_FIELDS = {
"Invoices": {
"createdByMyApp": bool,
"summaryOnly": bool,
"IDs": list,
"InvoiceNumbers": list,
"ContactIDs": list,
"Statuses": list,
},
"PurchaseOrders": {"DateFrom": date, "DateTo": date, "Status": str},
"Quotes": {
"ContactID": UUID,
"ExpiryDateFrom": date,
"ExpiryDateTo": date,
"DateFrom": date,
"DateTo": date,
"Status": str,
"QuoteNumber": str,
},
"Journals": {"paymentsOnly": bool},
"Budgets": {"DateFrom": date, "DateTo": date},
"Contacts": {
"IDs": list,
"includeArchived": bool,
"summaryOnly": bool,
"searchTerm": str,
},
"TrackingCategories": {"includeArchived": bool},
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document valid filtering fields for Objects along with some basic typing

Comment on lines +430 to +442
def get_filter_value(key, value, value_type=None):
if key in self.BOOLEAN_FIELDS or value_type == bool:
return "true" if value else "false"
elif key in self.DATE_FIELDS or value_type == date:
return f"{value.year}-{value.month}-{value.day}"
elif key in self.DATETIME_FIELDS or value_type == datetime:
return value.isoformat()
elif key.endswith("ID") or value_type == UUID:
return "%s" % (
value.hex if type(value) == UUID else UUID(value).hex
)
else:
return value
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helper function to convert filter fields to the format expected by xero

Comment on lines +482 to +490
KNOWN_PARAMETERS = ["order", "offset", "page"]
object_params = self.OBJECT_FILTER_FIELDS.get(self.name, {})
LIST_PARAMETERS = list(
filter(lambda x: object_params[x] == list, object_params)
)
EXTRA_PARAMETERS = list(
filter(lambda x: object_params[x] != list, object_params)
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Differentiated processing for basic fields and "list" fields

Comment on lines +492 to +496
for param in KNOWN_PARAMETERS + EXTRA_PARAMETERS:
if param in kwargs:
params[param] = get_filter_value(
param, kwargs.pop(param), object_params.get(param, None)
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Processing of simple filter fields, with some basic typed conversion of input data

Comment on lines +498 to +505
for param in LIST_PARAMETERS:
if param in kwargs:
params[param] = kwargs.pop(param)
if param.endswith("IDs"):
params[param] = ",".join(
map(lambda x: UUID(x).hex, kwargs.pop(param))
)
else:
params[param] = ",".join(kwargs.pop(param))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Processing of list filter fields, with no input verification except for IDs that are interpreted as UUIDs.

Copy link
Owner

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the modification to the test that you've identified - but otherwise, this looks good to merge. Thanks for the contribution!

@@ -8,7 +8,7 @@ def __init__(self, name, credentials, unit_price_4dps=False, user_agent=None):
from xero import __version__ as VERSION # noqa

self.credentials = credentials
self.name = name.capitalize()
self.name = name.capitalize() if name.islower() else name
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The problem is with the test, not the manager.

@freakboy3742 freakboy3742 merged commit b617239 into freakboy3742:main Sep 30, 2023
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants