-
Notifications
You must be signed in to change notification settings - Fork 208
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
Conversation
…akboy3742#339 - undocumented xero error messaging for list filters
…case, but basemanager assumes capitalized, so force capitalization in initialisation
src/xero/manager.py
Outdated
@@ -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 |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
src/xero/basemanager.py
Outdated
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}, | ||
} |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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
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) | ||
) | ||
|
There was a problem hiding this comment.
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
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) | ||
) |
There was a problem hiding this comment.
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
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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!
src/xero/manager.py
Outdated
@@ -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 |
There was a problem hiding this comment.
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.
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:
It also add some typing and basic input validation on filter fields, e.g.
Finally, it also resolves issue #339.