GithubHelp home page GithubHelp logo

freakboy3742 / pyxero Goto Github PK

View Code? Open in Web Editor NEW
279.0 33.0 208.0 572 KB

Python API for accessing the REST API of the Xero accounting tool.

License: BSD 3-Clause "New" or "Revised" License

Python 100.00%

pyxero's Introduction

PyXero

Python Versions PyPI Version Maturity BSD License Build Status

PyXero is a Python API for accessing the REST API provided by the Xero accounting tool. It allows access to both Public, Private and Partner applications.

Quickstart:

Install this library using the python package manager:

pip install pyxero

Using OAuth2 Credentials

OAuth2 is an open standard authorization protocol that allows users to provide specific permissions to apps that want to use their account. OAuth2 authentication is performed using tokens that are obtained using an API; these tokens are then provided with each subsequent request.

OAuth2 tokens have a 30 minute expiry, but can be swapped for a new token at any time. Xero documentation on the OAuth2 process can be found here. The procedure for creating and authenticating credentials is as follows (with a Django example at the end):

  1. Register your app with Xero, using a redirect URI which will be served by your app in order to complete the authorisation e.g. https://mysite.com/oauth/xero/callback/. See step 3 for an example of what your app should do. Generate a Client Secret, then store it and the Client Id somewhere that your app can access them, such as a config file.

  2. Construct an OAuth2Credentials instance using the details from the first step.

    >>> from xero.auth import OAuth2Credentials
    >>>
    >>> credentials = OAuth2Credentials(client_id, client_secret,
    >>>                                 callback_uri=callback_uri)

    If neccessary pass in a list of scopes to define the scopes required by your app. E.g. if write access is required to transactions and payroll employees:

    >>> from xero.constants import XeroScopes
    >>>
    >>> my_scope = [XeroScopes.ACCOUNTING_TRANSACTIONS,
    >>>             XeroScopes.PAYROLL_EMPLOYEES]
    >>> credentials = OAuth2Credentials(client_id, client_secret, scope=my_scope
    >>>                                 callback_uri=callback_uri)

    The default scopes are ['offline_access', 'accounting.transactions.read', 'accounting.contacts.read']. offline_access is required in order for tokens to be refreshable. For more details on scopes see Xero's documentation.

  3. Generate a Xero authorisation url which the user can visit to complete authorisation. Then store the state of the credentials object and redirect the user to the url in their browser.

    >>> authorisation_url = credentials.generate_url()
    >>>
    >>> # Now store credentials.state somewhere accessible, e.g a cache
    >>> mycache['xero_creds'] = credentials.state
    >>>
    >>> # then redirect the user to authorisation_url
    ...

    The callback URI should be the redirect URI you used in step 1.

  4. After authorization the user will be redirected from Xero to the callback URI provided in step 1, along with a querystring containing the authentication secret. When your app processes this request, it should pass the full URI including querystring to verify():

    >>> # Recreate the credentials object
    >>> credentials = OAuth2Credentials(**mycache['xero_creds'])
    >>>
    >>> # Get the full redirect uri from the request including querystring
    >>> # e.g. request_uri = 'https://mysite.com/oauth/xero/callback/?code=0123456789&scope=openid%20profile&state=87784234sdf5ds8ad546a8sd545ss6'
    >>>
    >>> credentials.verify(request_uri)

    A token will be fetched from Xero and saved as credentials.token. If the credentials object needs to be created again either dump the whole object using:

    >>> cred_state = credentials.state
    >>> ...
    >>> new_creds = OAuth2Credentials(**cred_state)

    or just use the client_id, client_secret and token (and optionally scopes and tenant_id):

    >>> token = credentials.token
    >>> ...
    >>> new_creds = OAuth2Credentials(client_id, client_secret, token=token)
  5. Now the credentials may be used to authorize a Xero session. As OAuth2 allows authentication for multiple Xero Organisations, it is necessary to set the tenant_id against which the xero client's queries will run.

    >>> from xero import Xero
    >>> # Use the first xero organisation (tenant) permitted
    >>> credentials.set_default_tenant()
    >>> xero = Xero(credentials)
    >>> xero.contacts.all()
    >>> ...

    If the scopes supplied in Step 2 did not require access to organisations (e.g. when only requesting scopes for single sign) it will not be possible to make requests with the Xero API and set_default_tenant() will raise an exception.

    To pick from multiple possible Xero organisations the tenant_id may be set explicitly:

    >>> tenants = credentials.get_tenants()
    >>> credentials.tenant_id = tenants[1]['tenantId']
    >>> xero = Xero(credentials)

    OAuth2Credentials.__init__() accepts tenant_id as a keyword argument.

  6. When using the API over an extended period, you will need to exchange tokens when they expire. If a refresh token is available, it can be used to generate a new token:

    >>> if credentials.expired():
    >>>     credentials.refresh()
    >>>     # Then store the new credentials or token somewhere for future use:
    >>>     cred_state = credentials.state
    >>>     # or
    >>>     new_token = credentials.token
    
    **Important**: ``credentials.state`` changes after a token swap. Be sure to
    persist the new state.

Django OAuth2 App Example

This example shows authorisation, automatic token refreshing and API use in a Django app which has read/write access to contacts and transactions. If the cache used is cleared on server restart, the token will be lost and verification will have to take place again.

from django.http import HttpResponseRedirect
from django.core.cache import caches

from xero import Xero
from xero.auth import OAuth2Credentials
from xero.constants import XeroScopes

def start_xero_auth_view(request):
   # Get client_id, client_secret from config file or settings then
   credentials = OAuth2Credentials(
       client_id, client_secret, callback_uri=callback_uri,
       scope=[XeroScopes.OFFLINE_ACCESS, XeroScopes.ACCOUNTING_CONTACTS,
              XeroScopes.ACCOUNTING_TRANSACTIONS]
   )
   authorization_url = credentials.generate_url()
   caches['mycache'].set('xero_creds', credentials.state)
   return HttpResponseRedirect(authorization_url)

def process_callback_view(request):
   cred_state = caches['mycache'].get('xero_creds')
   credentials = OAuth2Credentials(**cred_state)
   auth_secret = request.build_absolute_uri()
   credentials.verify(auth_secret)
   credentials.set_default_tenant()
   caches['mycache'].set('xero_creds', credentials.state)

def some_view_which_calls_xero(request):
   cred_state = caches['mycache'].get('xero_creds')
   credentials = OAuth2Credentials(**cred_state)
   if credentials.expired():
       credentials.refresh()
       caches['mycache'].set('xero_creds', credentials.state)
   xero = Xero(credentials)

   contacts = xero.contacts.all()
   ...

Using PKCE Credentials

PKCE is an alternative flow for providing authentication via OAuth2. It works largely the same as the standard OAuth2 mechanism, but unlike the normal flow is designed to work with applications which cannot keep private keys secure, such as desktop, mobile or single page apps where such secrets could be extracted. A client ID is still required.

As elsewhere, OAuth2 tokens have a 30 minute expiry, but can be only swapped for a new token if the offline_access scope is requested.

Xero documentation on the PKCE flow can be found here. The procedure for creating and authenticating credentials is as follows (with a CLI example at the end):

  1. Register your app with Xero, using a redirect URI which will be served by your app in order to complete the authorisation e.g. http://localhost:<port>/callback/. You can chose any port, anc can pass it to the credentials object on construction, allow with the the Client Id you are provded with.

  2. Construct an OAuth2Credentials instance using the details from the first step.

    >>> from xero.auth import OAuth2Credentials
    >>>
    >>> credentials = OAuth2PKCECredentials(client_id,   port=my_port)

    If neccessary, pass in a list of scopes to define the scopes required by your app. E.g. if write access is required to transactions and payroll employees:

    >>> from xero.constants import XeroScopes
    >>>
    >>> my_scope = [XeroScopes.ACCOUNTING_TRANSACTIONS,
    >>>             XeroScopes.PAYROLL_EMPLOYEES]
    >>> credentials = OAuth2Credentials(client_id, scope=my_scope
    >>>                                 port=my_port)

    The default scopes are ['offline_access', 'accounting.transactions.read', 'accounting.contacts.read']. offline_access is required in order for tokens to be refreshable. For more details on scopes see Xero's documentation on oAuth2 scopes.

  3. Call credentials.logon() . This will open a browser window, an visit a Xero authentication page.

    >>> credentials.logon()

    The Authenticator will also start a local webserver on the provided port. This webserver will be used to collect the tokens that Xero returns.

    The default PCKEAuthReceiver class has no reponse pages defined so the browser will show an error, on empty page for all transactions. But the application is now authorised and will continue. If you wish you can override the send_access_ok() method, and the send_error_page() method to create a more userfriendly experience.

    In either case once the callback url has been visited the local server will shutdown.

  4. You can now continue as per the normal OAuth2 flow. Now the credentials may be used to authorize a Xero session. As OAuth2 allows authentication for multiple Xero Organisations, it is necessary to set the tenant_id against which the xero client's queries will run.

    >>> from xero import Xero
    >>> # Use the first xero organisation (tenant) permitted
    >>> credentials.set_default_tenant()
    >>> xero = Xero(credentials)
    >>> xero.contacts.all()
    >>> ...

    If the scopes supplied in Step 2 did not require access to organisations (e.g. when only requesting scopes for single sign) it will not be possible to make requests with the Xero API and set_default_tenant() will raise an exception.

    To pick from multiple possible Xero organisations the tenant_id may be set explicitly:

    >>> tenants = credentials.get_tenants()
    >>> credentials.tenant_id = tenants[1]['tenantId']
    >>> xero = Xero(credentials)

    OAuth2Credentials.__init__() accepts tenant_id as a keyword argument.

  5. When using the API over an extended period, you will need to exchange tokens when they expire. If a refresh token is available, it can be used to generate a new token:

    >>> if credentials.expired():
    >>>     credentials.refresh()
    >>>     # Then store the new credentials or token somewhere for future use:
    >>>     cred_state = credentials.state
    >>>     # or
    >>>     new_token = credentials.token
    
    **Important**: ``credentials.state`` changes after a token swap. Be sure to
    persist the new state.

CLI OAuth2 App Example

This example shows authorisation, automatic token refreshing and API use in a Django app which has read/write access to contacts and transactions.

Each time this app starts it asks for authentication, but you could consider using the user keyring to store tokens.

from xero import Xero
from xero.auth import OAuth2PKCECredentials
from xero.constants import XeroScopes

# Get client_id, client_secret from config file or settings then
credentials = OAuth2PKCECredentials(
    client_id, port=8080,
    scope=[XeroScopes.OFFLINE_ACCESS, XeroScopes.ACCOUNTING_CONTACTS,
            XeroScopes.ACCOUNTING_TRANSACTIONS]
)
credentials.logon()
credentials.set_default_tenant()

for contacts in xero.contacts.all()
    print contact["Name"]

Older authentication methods

In the past, Xero had the concept of "Public", "Private", and "Partner" applications, which each had their own authentication procedures. However, they removed access for Public applications on 31 March 2021; Private applications were removed on 30 September 2021. Partner applications still exist, but the only supported authentication method is OAuth2; these are now referred to as "OAuth2 apps". As Xero no longer supports these older authentication methods, neither does PyXero.

Using the Xero API

This API is a work in progress. At present, there is no wrapper layer to help create real objects, it just returns dictionaries in the exact format provided by the Xero API. This will change into a more useful API before 1.0

The Xero API object exposes a simple API for retrieving and updating objects. For example, to deal with contacts::

# Retrieve all contact objects
>>> xero.contacts.all()
[{...contact info...}, {...contact info...}, {...contact info...}, ...]

# Retrieve a specific contact object
>>> xero.contacts.get(u'b2b5333a-2546-4975-891f-d71a8a640d23')
{...contact info...}

# Retrieve all contacts updated since 1 Jan 2013
>>> xero.contacts.filter(since=datetime(2013, 1, 1))
[{...contact info...}, {...contact info...}, {...contact info...}]

# Retrieve all contacts whose name is 'John Smith'
>>> xero.contacts.filter(Name='John Smith')
[{...contact info...}, {...contact info...}, {...contact info...}]

# Retrieve all contacts whose name starts with 'John'
>>> xero.contacts.filter(Name__startswith='John')
[{...contact info...}, {...contact info...}, {...contact info...}]

# Retrieve all contacts whose name ends with 'Smith'
>>> xero.contacts.filter(Name__endswith='Smith')
[{...contact info...}, {...contact info...}, {...contact info...}]

# Retrieve all contacts whose name starts with 'John' and ends with 'Smith'
>>> xero.contacts.filter(Name__startswith='John', Name__endswith='Smith')
[{...contact info...}, {...contact info...}, {...contact info...}]

# Retrieve all contacts whose name contains 'mit'
>>> xero.contacts.filter(Name__contains='mit')
[{...contact info...}, {...contact info...}, {...contact info...}]

# Create a new object
>>> xero.contacts.put({...contact info...})

# Create multiple new objects
>>> xero.contacts.put([{...contact info...}, {...contact info...}, {...contact info...}])

# Save an update to an existing object
>>> c = xero.contacts.get(u'b2b5333a-2546-4975-891f-d71a8a640d23')
>>> c['Name'] = 'John Smith'
>>> xero.contacts.save(c)

# Save multiple objects
>>> xero.contacts.save([c1, c2])

Complex filters can be constructed in the Django-way, for example retrieving invoices for a contact:

>>> xero.invoices.filter(Contact_ContactID='83ad77d8-48a7-4f77-9146-e6933b7fb63b')

Filters which aren't supported by this API can also be constructed using 'raw' mode like this:

>>> xero.invoices.filter(raw='AmountDue > 0')

Be careful when dealing with large amounts of data, the Xero API will take an increasingly long time to respond, or an error will be returned. If a query might return more than 100 results, you should make use of the page parameter::

# Grab 100 invoices created after 01-01-2013
>>> xero.invoices.filter(since=datetime(2013, 1, 1), page=1)

You can also order the results to be returned::

# Grab contacts ordered by EmailAddress
>>> xero.contacts.filter(order='EmailAddress DESC')

For invoices (and other objects that can be retrieved as PDFs), accessing the PDF is done via setting the Accept header:

# Fetch a PDF
invoice = xero.invoices.get('af722e93-b64f-482d-9955-1b027bfec896', \
    headers={'Accept': 'application/pdf'})
# Stream the PDF to the user (Django specific example)
response = HttpResponse(invoice, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="invoice.pdf"'
return response

Download and uploading attachments is supported using the Xero GUID of the relevant object::

# List attachments on a contact
>>> xero.contacts.get_attachments(c['ContactID'])
[{...attachment info...}, {...attachment info...}]

# Attach a PDF to a contact
>>> f = open('form.pdf', 'rb')
>>> xero.contacts.put_attachment(c['ContactID'], 'form.pdf', f, 'application/pdf')
>>> f.close()

>>> xero.contacts.put_attachment_data(c['ContactID'], 'form.pdf', data, 'application/pdf')

# Download an attachment
>>> f = open('form.pdf', 'wb')
>>> xero.contacts.get_attachment(c['ContactID'], 'form.pdf', f)
>>> f.close()

>>> data = xero.contacts.get_attachment_data(c['ContactID'], 'form.pdf')

This same API pattern exists for the following API objects:

  • Accounts
  • Attachments
  • BankTransactions
  • BankTransfers
  • BrandingThemes
  • ContactGroups
  • Contacts
  • CreditNotes
  • Currencies
  • Employees
  • ExpenseClaims
  • Invoices
  • Items
  • Journals
  • ManualJournals
  • Organisation
  • Overpayments
  • Payments
  • Prepayments
  • Purchase Orders
  • Receipts
  • RepeatingInvoices
  • Reports
  • TaxRates
  • TrackingCategories
  • Users

Payroll

In order to access the payroll methods from Xero, you can do it like this:

xero.payrollAPI.payruns.all()

Within the payrollAPI you have access to:

  • employees
  • leaveapplications
  • payitems
  • payrollcalendars
  • payruns
  • payslip
  • superfunds
  • timesheets

Projects

In order to access the projects methods from Xero, you can do it like this:

xero.projectsAPI.projects.all()

Within the projectsAPI you have access to:

  • projects
  • projectsusers
  • tasks
  • time

Under the hood

Using a wrapper around Xero API is a really nice feature, but it's also interesting to understand what is exactly happening under the hood.

Filter operator

filter operator wraps the "where" keyword in Xero API.

# Retrieves all contacts whose name is "John"
>>> xero.contacts.filter(name="John")

# Triggers this GET request:
Html encoded: <XERO_API_URL>/Contacts?where=name%3D%3D%22John%22
Non encoded:  <XERO_API_URL>/Contacts?where=name=="John"

Several parameters are separated with encoded '&&' characters:

# Retrieves all contacts whose first name is "John" and last name is "Doe"
>>> xero.contacts.filter(firstname="John", lastname="Doe")

# Triggers this GET request:
Html encoded: <XERO_API_URL>/Contacts?where=lastname%3D%3D%22Doe%22%26%26firstname%3D%3D%22John%22
Non encoded:  <XERO_API_URL>/Contacts?where=lastname=="Doe"&&firstname=="John"

Underscores are automatically converted as "dots":

# Retrieves all contacts whose name is "John"
>>> xero.contacts.filter(first_name="John")

# Triggers this GET request:
Html encoded: <XERO_API_URL>/Contacts?where=first.name%3D%3D%22John%22%
Non encoded:  <XERO_API_URL>/Contacts?where=first.name=="John"

Contributing

If you're going to run the PyXero test suite, in addition to the dependencies for PyXero, you need to add the following dependency to your environment:

mock >= 1.0

Mock isn't included in the formal dependencies because they aren't required for normal operation of PyXero. It's only required for testing purposes.

Once you've installed these dependencies, you can run the test suite by running the following from the root directory of the project:

$ tox -e py

If you find any problems with PyXero, you can log them on Github Issues. When reporting problems, it's extremely helpful if you can provide reproduction instructions -- the sequence of calls and/or test data that can be used to reproduce the issue.

New features or bug fixes can be submitted via a pull request. If you want your pull request to be merged quickly, make sure you either include regression test(s) for the behavior you are adding/fixing, or provide a good explanation of why a regression test isn't possible.

pyxero's People

Contributors

aidanlister avatar albertyw avatar ari avatar bartonip avatar bertildaniel avatar dependabot[bot] avatar direvus avatar evrimoztamur avatar fluffels avatar freakboy3742 avatar gavinhodge avatar himynameistimli avatar jacobg avatar jarekwg avatar jneves avatar jstevans avatar matthealy avatar mjmortimer avatar nicois avatar peircej avatar rgammans avatar romgar avatar schapman1974 avatar schinckel avatar sidneyallen avatar thisismyrobot avatar timrichardson avatar tkav avatar uber1geek avatar vadim-pavlov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pyxero's Issues

Organisation/Organisations have a slightly quirky response

Just started using the pyxero API, and running into a few kinks, mostly related to getting the details re: the current Organisation that we're connecting to.

To fix this, I had to make a small change of Organisation to Organisations in api.py.....

At that point things started working.....

I haven't worked out why this change is seemingly needed....

TooManyRedirects: Exceeded 30 redirects. on google appengine

Hi,

I'm attempting to use the pyxero from google appengine. I've added a bunch of dependencies into my appengine app (requests_oauthlib, oauthlib, xero, dateutil, requests), and then execute the following code:

    from xero.api import Xero
    from xero.auth import PrivateCredentials
    with open(RSA_KEY_FILE) as keyfile:
        rsa_key = keyfile.read()
    credentials = PrivateCredentials(CONSUMER_KEY, rsa_key)
    xero = Xero(credentials)
    all_contacts = xero.contacts.all()

I get an error:

  File "/Users/hamish/dev/gae_xero/xerointegration.py", line 43, in get
    all_contacts = xero.contacts.all()
  File "/Users/hamish/dev/gae_xero/xero/manager.py", line 167, in wrapper
    response = getattr(requests, method)(uri, data=body, headers=headers, auth=self.oauth)
  File "/Users/hamish/dev/gae_xero/requests/api.py", line 55, in get
    return request('get', url, **kwargs)
  File "/Users/hamish/dev/gae_xero/requests/api.py", line 44, in request
    return session.request(method=method, url=url, **kwargs)
  File "/Users/hamish/dev/gae_xero/requests/sessions.py", line 391, in request
    resp = self.send(prep, **send_kwargs)
  File "/Users/hamish/dev/gae_xero/requests/sessions.py", line 522, in send
    history = [resp for resp in gen] if allow_redirects else []
  File "/Users/hamish/dev/gae_xero/requests/sessions.py", line 94, in resolve_redirects
    raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects)
TooManyRedirects: Exceeded 30 redirects.

I've added logging in the lower level libraries, and have noticed that the first connection attempts to connect to:
https://api.xero.com/api.xro/2.0/Contacts
whereas the following connections go to:
https://api.xero.com

i also noticed that if I send an empty string instead of the consumer_key, i get the same 30 retry error. I think I'm sending the correct value, but it's always a chance that I've made a mistake somewhere

I've uploaded my testing app to github:
https://github.com/hamish/gae_xero

Any help you can give would be awesome.

pyxero is broken when used on partner application

I've used the official Xero PHP sdk and our entrust client ssl keys, which is working to get through the 3 legged oauth for partner apps. However when I used pyxero with the same entrust client ssl, same keys and secret, it does not work, always returns a 403 when the oauth dance starts. Did someone already having this issue?

XeroBadRequest

Hey guys :)

Just wondering if it's possible to extract the validation errors for a request that has failed due to validation problems. Currently, when it throws a "XeroBadRequest" - it just says "Request was invalid" etc. To see what the problem was, I have to login to Xero and check out the Api history for the App (I'm using the test company).

Is it possible to retreive those errors?

My apologies if I'm doing something wrong and it's already implemented.

Cheers,
Ben

0.6.0 release date?

Hi there,

I was wondering when 0.6.0 and it's new attachment features will be officially released?

I know I can import the release candidate into our project but I thought it might be worth checking first to see if the official module is going to be updated soon.

Thanks, Joel :)

Invoice: LineItem with blanks fields causes errors

I'm using HEAD and a test account to get my Xero integration working but I ran into a problem when I created a blank invoice. Even though I never added it explicitly, the invoice had a single line item with all blank fields. Trying to request that invoice caused an error:

>>> inv = xero.invoices.get('<invoice id>')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 220, in wrapper
    data = self.convert_to_dict(self.walk_dom(dom))
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 119, in convert_to_dict
    data = self.convert_to_dict(deep_list[1])
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 110, in convert_to_dict
    data = self.convert_to_dict(data)
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 119, in convert_to_dict
    data = self.convert_to_dict(deep_list[1])
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 110, in convert_to_dict
    data = self.convert_to_dict(data)
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 119, in convert_to_dict
    data = self.convert_to_dict(deep_list[1])
  File "/home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py", line 128, in convert_to_dict
    out = deep_list[0]
IndexError: tuple index out of range
>>> import pdb
>>> pdb.pm()
> /home/vagrant/.virtualenvs/app/src/pyxero-master/xero/manager.py(128)convert_to_dict()
-> out = deep_list[0]
(Pdb) deep_list
()

The XML as returned by the API Previewer:

<LineItems>
    <LineItem />
</LineItems>

It seems Manager.convert_to_dict is mishandling the self-closing tag.

Error When Filtering on GUID

When filtering on Contact_ContactID, Pyxero adds the Guid wrapper, however the operator becomes '==' which is not allowed. It needs to be '='. This is the error:

Operator '==' incompatible with operand types 'Guid' and 'String'

Is this perhaps that I'm "not holding right"?

Error handling for error 400 not correct in manager.py

The line reads elif response.status_code == 400 or response.status_code == 401: and handles both 400 and 401 responses. It expects oauth_problem to be present. However, that one only exists in the 401 case. 400 seems to be used for validation error where you get a giant XML as a response.

Issues with RSAAlgorithm when trying to use private or partner apps

Hey guys, I'm testing using pyxero with different application types and for both private and partner I'm seeing the following error and am wondering if you have seen it before or know why?

Traceback (most recent call last):
File "", line 1, in
File "xero\auth.py", line 350, in init
self.init_credentials(oauth_token, oauth_token_secret)
File "xero\auth.py", line 146, in init_credentials
response = requests.post(url=url, auth=oauth, cert=self.client_cert)
File "c:\Python27\lib\site-packages\requests-2.7.0-py2.7.egg\requests\api.py", line 109, in post
return request('post', url, data=data, json=json, *kwargs)
File "c:\Python27\lib\site-packages\requests-2.7.0-py2.7.egg\requests\api.py", line 50, in request
response = session.request(method=method, url=url, *kwargs)
File "c:\Python27\lib\site-packages\requests-2.7.0-py2.7.egg\requests\sessions.py", line 451, in request
prep = self.prepare_request(req)
File "c:\Python27\lib\site-packages\requests-2.7.0-py2.7.egg\requests\sessions.py", line 382, in prepare_request
hooks=merge_hooks(request.hooks, self.hooks),
File "c:\Python27\lib\site-packages\requests-2.7.0-py2.7.egg\requests\models.py", line 308, in prepare
self.prepare_auth(auth, url)
File "c:\Python27\lib\site-packages\requests-2.7.0-py2.7.egg\requests\models.py", line 496, in prepare_auth
r = auth(self)
File "c:\Python27\lib\site-packages\requests_oauthlib-0.5.0-py2.7.egg\requests_oauthlib\oauth1_auth.py", line 87, in call
unicode(r.url), unicode(r.method), None, r.headers)
File "c:\Python27\lib\site-packages\oauthlib\oauth1\rfc5849__init
.py", line 313, in sign
('oauth_signature', self.get_oauth_signature(request)))
File "c:\Python27\lib\site-packages\oauthlib\oauth1\rfc5849__init
.py", line 150, in get_oauth_signature
sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
File "c:\Python27\lib\site-packages\oauthlib\oauth1\rfc5849\signature.py", line 505, in sign_rsa_sha1_with_client
return sign_rsa_sha1(base_string, client.rsa_key)
File "c:\Python27\lib\site-packages\oauthlib\oauth1\rfc5849\signature.py", line 496, in sign_rsa_sha1
alg = _jwt_rs1_signing_algorithm()
File "c:\Python27\lib\site-packages\oauthlib\oauth1\rfc5849\signature.py", line 474, in _jwt_rs1_signing_algorithm
_jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1)
AttributeError: 'module' object has no attribute 'RSAAlgorithm'

Lists of 1 item are sometimes transformed to singletons

Using the demo company and interactive API, when I pull invoice details for 86d6e00f-ef56-49f7-9a54-796ccd5ca057 (in JSON view), it contains:
"LineItems": [
{
"ItemCode": "PMD",
"Description": "Project Management for restructure - onsite daily rate",
...

However, the object returned via pyxero using invoice.get(...) includes:
'LineItems': {'LineItem': {'AccountCode': '200', ...

I am not sure what happens with an invoice with more than one line item. The demo organisation does not contain such invoices. I am also not sure if this is the only occurance, or if other entries are also collapsed.
In any event, there should be a list of LineItem objects, rather than a single bare dict.

Tests don't run

Can't run tests with python setup.py test inside pyxero git checkout. Is it the wrong way to run tests?

$ python setup.py test
running test
running egg_info
writing requirements to pyxero.egg-info/requires.txt
writing pyxero.egg-info/PKG-INFO
writing top-level names to pyxero.egg-info/top_level.txt
writing dependency_links to pyxero.egg-info/dependency_links.txt
reading manifest file 'pyxero.egg-info/SOURCES.txt'
writing manifest file 'pyxero.egg-info/SOURCES.txt'
running build_ext
Traceback (most recent call last):
  File "setup.py", line 37, in <module>
    test_suite="tests",
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/distutils/core.py", line 152, in setup
    dist.run_commands()
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/distutils/dist.py", line 953, in run_commands
    self.run_command(cmd)
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/Users/shamrin/src/xero/venv/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/setuptools/command/test.py", line 121, in run
  File "/Users/shamrin/src/xero/venv/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/setuptools/command/test.py", line 101, in with_project_on_sys_path
  File "/Users/shamrin/src/xero/venv/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/setuptools/command/test.py", line 130, in run_tests
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/main.py", line 94, in __init__
    self.parseArgs(argv)
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/main.py", line 149, in parseArgs
    self.createTests()
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/main.py", line 158, in createTests
    self.module)
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 128, in loadTestsFromNames
    suites = [self.loadTestsFromName(name, module) for name in names]
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 103, in loadTestsFromName
    return self.loadTestsFromModule(obj)
  File "/Users/shamrin/src/xero/venv/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/setuptools/command/test.py", line 34, in loadTestsFromModule
  File "/usr/local/Cellar/python/2.7.5/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 100, in loadTestsFromName
    parent, obj = obj, getattr(obj, part)
AttributeError: 'module' object has no attribute 'auth'

A validation exception occurred

I'm trying to do the simplest example to create an invoice

with open("/home/b0ef/.ssh/privatekey-xero.pem") as keyfile: rsa_key = keyfile.read()
credentials = PrivateCredentials("NA", rsa_key)
xero = Xero(credentials)
original = {
'Invoice': {
'Type': 'ACCREC',
'Contact': {'ContactID': '3e776c4b-ea9e-4bb1-96be-6b0c7a71a37f'},
'Date': date(2014, 7, 8),
'DueDate': date(2014, 7, 24),
'InvoiceNumber': 'X0010',
'Status': 'AUTHORISED', #I added this
'LineItems': [
{
'Description': 'Line item 1',
'Quantity': '1.0',
'UnitAmount': '100.00',
'AccountCode': '200',
},
{
'Description': 'Line item 2',
'Quantity': '2.0',
'UnitAmount': '750.00',
'AccountCode': '200',
},
],

}

}

xero.invoices.put(original)

, but I get:

File "./xero_invoice_pyxero.py", line 50, in
xero.invoices.put(original)
File "build/bdist.linux-i686/egg/xero/manager.py", line 167, in wrapper
xero.exceptions.XeroBadRequest: A validation exception occurred

If I do this
xml = xero.invoices._prepare_data_for_save(original['Invoice'])
print xml

I get:

<Invoice><Status>AUTHORISED</Status><LineItems><LineItem><AccountCode>200</AccountCode><UnitAmount>100.00</UnitAmount><Description>Line item 1</Description><Quantity>1.0</Quantity></LineItem><LineItem><AccountCode>200</AccountCode><UnitAmount>750.00</UnitAmount><Description>Line item 2</Description><Quantity>2.0</Quantity></LineItem></LineItems><Contact><ContactID>3e776c4b-ea9e-4bb1-96be-6b0c7a71a37f</ContactID></Contact><Date>2014-07-08</Date><InvoiceNumber>X0010</InvoiceNumber><Type>ACCREC</Type><DueDate>2014-07-24</DueDate></Invoice>

..which seems to be valid to me.

Any idea what I can try?

setyup.py references to python-dateutil through xero.VERSION

When installing the package with pip from git, I get this error:
Running setup.py egg_info for package pyxero Traceback (most recent call last): File "<string>", line 16, in <module> File "/app/.heroku/src/pyxero/setup.py", line 3, in <module> from xero import VERSION File "xero/__init__.py", line 1, in <module> from .api import Xero File "xero/api.py", line 1, in <module> from .manager import Manager File "xero/manager.py", line 4, in <module> from dateutil.parser import parse ImportError: No module named dateutil.parser

SSL error using Google App Engine

I'm using Google App Engine (1.9.17) and I am receiving a SSL error as follows:

TypeError: must be _socket.socket, not socket

I am import ssl in the yaml as follows:

- name: ssl
  version: latest

The same issue has been resolved for the Stripe API. Link

Note: This does not happen when deployed but it happens when using the dev server.

Any chance something like this could happen with PyXero? Anyone know a workaround?

Call Back Does not Work

Hello,

I have tried using the callback uri feature and after authentication the user is not passed back to the site.

Any ideas?

Thanks,

Nick

Always return collections for collection-typed objects.

Consider:

<Foos>
    <Foo>
        <Bar>baz</Bar>
        <Qux>quux</Qux>
    </Foo>
</Foos>

Currently, this returns:

{'Foos':{'Foo':{ ... } } }

As opposed to:

<Foos>
    <Foo>
        <Bar>baz</Bar>
        <Qux>quux</Qux>
    </Foo>
    <Foo>
        <Bar>wibble</Bar>
        <Qux>wobble</Qux>
    </Foo>
</Foos>

Which returns:

{'Foos': [{'Foo': {...}}, {'Foo': {...}}]}

I think that in both cases a list of dicts should be returned, even if said list only contains one dict.

PDF is returned as a Unicode object

Hi,

In manager.py, def get_data(self, func):

        if response.status_code == 200:
            if response.headers['content-type'] == 'application/pdf':
                return response.text

the above code returns a Unicode object (because requests guesses the response encoding and decodes the response):

When you make a request, Requests makes educated guesses about the encoding of
the response based on the HTTP headers. The text encoding guessed by Requests 
is used when you access r.text. 

In case of binary files the decoding/re-encoding breaks things - the resulting PDF is broken. Example script:

from xero import Xero
from xero.auth import PrivateCredentials

credentials = PrivateCredentials(XERO_CONSUMER_KEY, XERO_PRIVATE_KEY_FILE)
x = Xero(credentials)
invoice = x.invoices.get(INVOICE_ID, headers={'Accept': 'application/pdf'})

with open('invoice.pdf', 'w') as f:
    f.write(invoice.encode('utf-8'))  # can't write without encoding

PROPOSED FIX: Replacing return response.text with return response.content solves the issue for me. requests docs suggest that .content should be used for binary responses.

(Python 2.7)

New Line in Description Field

I am creating a invoice and adding line items. Is there away to have a line break in the description? For example the description would look like:

This is a product

And it is helpful

Any ideas?

xero.invoices.put returns a list now in v0.6.0-rc.1?

Hi there,

I've just been testing v0.6.0-rc.1 on our scripts here and it's working great. The only thing I've noticed is that xero.invoices.put now returns a list instead of a dict. Is that what you meant by "Ensure that filter() always returns a list" ???

If this is all as expected, no problems. Wanted to suggest that you mention it in the release notes somewhere explicitily for us Python newbies :D

Also, a big thank you to @bertilnilsson for the new attachment support. It's exactly what we need for our current project.

Thanks, Joel

Possible to Append an existing invoice with additional Line Items

I could not find any reference as to how one would edit an existing invoice and / or add line items to it. Our use case is that we have some repeating invoices, and we want to add some variable costs from timesheets onto them before sending them out.

My test:

ipdb> items = invoice['LineItems']
ipdb> invoice['LineItems'] = items + items
ipdb> xero.invoices.save(invoice)
*** XeroBadRequest: Object reference not set to an instance of an object.

Reduce verbosity in collections.

The Xero API is quite verbose (and generally consistent), where it returns structures that look like:

<Foos>
    <Foo>
        <Bar>baz</Bar>
        <Qux>quux</Qux>
    </Foo>
    <Foo>
        <Bar>wibble</Bar>
        <Qux>wobble</Qux>
    </Foo>
</Foos>

Currently, this generates a data structure that looks like:

{
    'Foos': [
        {'Foo': {'Bar':'baz','Qux':'quux'}},
        {'Foo': {'Bar':'wibble','Qux':'wobble'}},
    ]
}

To me, there is no need for the wrapper object 'Foo'. I would propose that it generates an output of:

{
    'Foos': [
        {'Bar':'baz','Qux':'quux'},
        {'Bar':'wibble','Qux':'wobble'},
    ]
}

Is there a reason I am not thinking of that the previous one is preferable?

Accessing validation errors

Hi there,

Is it possible to access validation error messages once an exception has been raised?

I'm hoping I can return that message in a notification email to the customer.

Thanks, Joel

10 ValidationException A validation exception occurred Account could not be found 2014-08-22T00:00:00 158.80 American Express - American Express - National Bank - Amex 00000000-0000-0000-0000-000000000000 100.3 11e7decc-8b6c-4c4a-bd02-7d921c5b0f61 Sales 2014-08-21T00:00:00 2014-08-21T00:00:00 AUTHORISED Inclusive 6088.28 575.13 6663.41 2014-08-21T02:47:44.403 AUD ACCREC f39e1997-a6b5-44a1-99fa-35ae721791f4 ORC1131 Fedelta 22/08/2014 - MYOB_GJ_21082014053003.TXT 6663.41 0.00 false 1.000000

Problem updating

Hey Guys,

I have the following code:

ci = xero.contacts.get(contact.xero_id)
ci['Name'] = 'whatever'
xero.contacts.save(ci)

Where xero is a connected private instance. It's returning back with:

XeroBadRequest: The string '2013-10-02 03:19:44.150000' is not a valid AllXsd value.

Which is referring to "UpdatedDateUTC". If I try and set the value of "UpdatedDateUTC" to datetime.datetime.now(), same error only with todays date.

Is there something I'm doing wrong?

Many thanks,
Ben

Version 0.7.0 pre-release checklist (JSON API)

I've just pushed up a change which eliminates the XML parsing for all data retrieval (GET, and the returned response from PUT/PATCH). This wipes out all the walk_dom crap which was causing all sorts of inconsistent responses (dicts instead of lists for singular items) and eliminates a tonne of complexity.

I've also cleaned up the xmlwriting part by explicitly translating between plural and singular verbs -- this will cause problems as I'm sure we haven't included all the mappings we need -- but it's better to have an explicit list rather than try and map between plurals when Xero isn't always consistent.

So, let's get some eyeballs on HEAD and see if it's stable enough to push out as a 0.7.0-alpha release in the next month.

  • Have some people use HEAD and confirm everything's working
  • Merge all PRs and close all remaining issues
  • More unit tests
  • Add examples for Public and Partner
  • Roll alpha release
  • Roll stable release

Partner credentials permission denied from Google AppEngine

Hello everyone,

Has anyone had issues trying to connect to Xero using Partner Certificates from Google AppEngine?

I allways get "permission denied", it is only from Google AppEngine, I've tried in other environment and it worked fine, but it seems that Google AppEngine take control of the url request at the last point and alters it adding something that Xero doesn't expect so they reject the connection attempt.

I've suggested to use sockets instead of a normal request, but even in that way I'm experiencing the same issue. I wonder if this last case has something to do with the certificate. There are aparently two ways of sending the certificate: one is as a tuple of the paths to the pair of pem files and another it seems to be possible to send a string that, I assume, it must be the content of both pem bundled (but I'm not sure...).
So, my questions are:
One: Has anyone got pyXero and Partnership program to Xero working on Google AppEngine? If so, how? What am I missing?
Two: what should be the content of the cert variable in "PartnerCredentials" class constructor if I chose to send the content of the client certificate instead of the tuple of the pair of pem files paths?

Thank you very much, I'd appreciate any kind of help.

Question about PartnerCredentials usage

I'm having some trouble trying to use PartnerCredentials with my app. I'm getting a 403 response during the POST /oauth/RequestToken request.

Here's my code. Are the keys I'm using correct?

XERO_RSA_KEY_PATH = 'xero-private.pem'  # my generated private key (http://developer.xero.com/documentation/advanced-docs/public-private-keypair/)
XERO_CERT_PATH = 'entrust-xero-cert.pem'  # the certificate split from .p12 file from Xero Entrust
XERO_PRIVATE_KEY_PATH = 'entrust-xero-private-nopass.pem'  # nopass from .p12 file from Xero Entrust

XERO_PARTNER_API_KEY = 'xxx'
XERO_PARTNER_API_SECRET = 'yyy'


def build_xero_credentials():
    with open(XERO_RSA_KEY_PATH) as f:
        rsa_key = f.read()
    client_cert = (XERO_CERT_PATH, XERO_PRIVATE_KEY_PATH)
    creds = PartnerCredentials(XERO_PARTNER_API_KEY,
                               XERO_PARTNER_API_SECRET,
                               rsa_key, client_cert)
    return creds

creds = build_xero_credentials()

And the logs:

D 2014-12-16 16:07:47.327
Signing request <PreparedRequest [POST]> using client <Client nonce=None, signature_method=RSA-SHA1, realm=None, encoding=utf-8, timestamp=None, resource_owner_secret=None, decoding=utf-8, verifier=None, signature_type=AUTH_HEADER, rsa_key=-----BEGIN RSA PRIVATE KEY-----
MIICX...
...NGQ==
-----END RSA PRIVATE KEY-----
, resource_owner_key=None, client_secret=None, callback_uri=None, client_key=>

D 2014-12-16 16:07:47.387
Including body in call to sign: False

D 2014-12-16 16:07:47.389
Collected params: [(u'oauth_version', u'1.0'), (u'oauth_consumer_key', u''), (u'oauth_signature_method', u'RSA-SHA1'), (u'oauth_nonce', u'1234567890'), (u'oauth_timestamp', u'1418717267')]

D 2014-12-16 16:07:47.390
Normalized params: oauth_consumer_key=&oauth_nonce=1234567890&oauth_signature_method=RSA-SHA1&oauth_timestamp=1418717267&oauth_version=1.0

D 2014-12-16 16:07:47.390
Normalized URI: https://api-partner.network.xero.com/oauth/RequestToken

D 2014-12-16 16:07:47.390
Base signing string: POST&https%3A%2F%2Fapi-partner.network.xero.com%2Foauth%2FRequestToken&oauth_consumer_key%3D%26oauth_nonce%3D1234567890%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1418717267%26oauth_version%3D1.0

D 2014-12-16 16:07:47.402
Signature: S30DavCz+Rrqpz/...co6A=

D 2014-12-16 16:07:47.403
Encoding URI, headers and body to utf-8.

D 2014-12-16 16:07:47.403
Updated url: https://api-partner.network.xero.com/oauth/RequestToken

D 2014-12-16 16:07:47.403
Updated headers: {'Content-Length': '0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'python-requests/2.4.3 CPython/2.7.5 Linux/', 'Connection': 'keep-alive', 'Authorization': 'OAuth oauth_nonce="1234567890", oauth_timestamp="1418717267", oauth_version="1.0", oauth_signature_method="RSA-SHA1", oauth_consumer_key="", oauth_signature="S30DavCz%2BRrqpz...co6A%3D"'}

D 2014-12-16 16:07:47.403
Updated body: None

I 2014-12-16 16:07:47.404
Starting new HTTPS connection (1): api-partner.network.xero.com

D 2014-12-16 16:07:47.983
"POST /oauth/RequestToken HTTP/1.1" 403 None

E 2014-12-16 16:07:47.984
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
<title>IIS 7.5 Detailed Error - 403.7 - Forbidden</title> 
</head> 
<body> 
 ... snipped ...
 See http://pastie.org/private/9hzu6awjkqxmrz88dhggw#47 for full body
 </body>
 </html>  

Did I miss anything?

Do I need to configure anything on my server? (I'm using Google AppEngine).
The Xero docs are a bit sparse, and only providing a guide for Microsoft IIS.

I decided to post here first, in case I just missed something or misunderstood how to use the library. I'll go to Xero support afterwards.

Thanks.

Signature Import Error

I'm getting a signature import error when I try to run a filter call on a Xero Manager object.

Any ideas? Am I missing something? I installed using pip install pyxero.

How to do an "OR" filter?

Hello, there!

I'm trying to write a query of contacts where the Account number could be one of anyone from a list... how can I do that with this pyxero library?

Thank you!

get() and filter() in trackingcategories not working

I have test with my code below, it working fine.

self.xero.trackingcategories.all()

However, when I try to filter it. it not working regardless which parameter i put in (always return None)

# all of these not working
self.xero.trackingcategories.get(u'f3a8bc1d-xxxx-xxxx-xxxx-ea3d696eac94')
self.xero.trackingcategories.get('f3a8bc1d-xxxx-xxxx-xxxx-ea3d696eac94')
self.xero.trackingcategories.filter(Name='Units')
self.xero.trackingcategories.filter(Name=u'Units')

Tested on Public Application

Response when something fails but not everything

Hello!

I have a case where I am posting invoices (a bunch of them) and some of them are right and they will be created, but one or more may be wrong... I'd like to have the possibility to get a response from the API that helps me to identify what's OK pushed and what's not... At the moment, if there is just one wrong invoice within x right ones, the library will throw an exception and it will give me a lot of information about the wrong one, but nothing about the right ones. If everything goes well, I should get the invoiceId along more information so I can identify what is well pushed and put the id on my side, but if there is something wrong... I can't because of the design of the exception... not sure if that's how Xero in itself responses the whole request, but if is in that way, something is not right there...

Can someone tell me if I am using it right or should I get more information from another way?
I am using just

try:
  res = xero.invoices.put([array_invoices]) 
except XeroException as e:
  res = e.__dict__

and then I read res... if everything is OK, res will have nice information about the new invoices created, if something goes wrong, it will throw an exception, reading the data in that exception I can figure out which part is wrong, but I can't know anything else about the ones that went OK and have been created on the Xero database propertly so I lost my track to them.

Thank you very much in advance!

Tracking Categories Not Returning

Hey!

I am using this call

xeroService.trackingcategories.all() 

to return all tracking categories for a organization but I am receiving None in response. I tried using the api previewer that Xero provides and I am able to retrieve tracking categories that way. Any help would be appreciated.

Can't search using a backslash or %2F

oauthlib is too strict!

  xero.contacts.filter(Name__contains='Jim C/- Ace Building Supplies')
  File "oauthlib/common.py", line 404, in __init__
      self._params.update(dict(urldecode(self.uri_query)))
  File "oauthlib/common.py", line 141, in urldecode
      raise ValueError(error % (set(query) - urlencoded, query))
  ValueError: Error trying to decode a non urlencoded string. Found invalid characters: set([u'/']) in the string: 'where=Name.contains%28%22E24/310%22%29'. Please ensure the request/response body is x-www-form-urlencoded.

It seems like it should be possible to do xero.contacts.filter(Name__contains='Jim C%2F- Ace Building Supplies') .. but it doesn't work.

Object Reference Error

I'm having some trouble with pushing invoices to xero with pyxero.

with open('PRIVATEKEY.pem') as keyfile:
 rsa_key = keyfile.read()

creds = PrivateCredentials("KEY", rsa_key)
xero = Xero(creds)

line_items = {"Description": "Sample","Quantity": "4", "UnitAmount": "15.00", "AccountCode": "400"}

test = {'Contact': {'Name': 'Test Name'}, 
        'Date': '2014-03-10',
        'DueDate': '2014-07-25',
        'LineItems': {'AccountCode': '400','Description': 'GB3-White','Quantity':'4','UnitAmount': '15.00'},
        'Status': 'DRAFT',
        'Type': 'ACCREC'}

xero.invoices.put(test)

After I run that, I receive this error:

xero.exceptions.XeroBadRequest: Object reference not set to an instance of an object.

I have the most up to date repo. Also when I run xero.accounts.all(), I'm able receive the accounts. Is something off with how I'm formulating the invoice?

Thanks!

Some fields are not correctly sent back to the server

The spec dictates the necessity for the format 2013-10-29T01:39:52.367 for xs:dateTime. It also dictates 1/0 for boolean fields.

When posting a datetime object received from pyxero back at the API, it doesn't convert back to this format, leading to a 400 error. This also happens with boolean True/False being converted to string and not being converted back.

Example request data:

<Invoice><TotalTax>90.0</TotalTax><LineItems><LineItem><LineAmount>450.0</LineAmount><TaxAmount>90.0</TaxAmount><UnitAmount>150.0</UnitAmount><Description>test Product</Description><Quantity>3.0</Quantity></LineItem></LineItems><Contact><TaxNumber>GB 234567890</TaxNumber><Name>Bayside Club</Name><FirstName>Bob</FirstName><UpdatedDateUTC>2013-10-29 01:39:52.367000</UpdatedDateUTC><LastName>Partridge</LastName><SkypeUserName>bayside577</SkypeUserName><IsSupplier>True</IsSupplier><ContactGroups><ContactGroup><ContactGroupID>91dbdc3f-86c5-4bfe-b227-5d1735945cea</ContactGroupID><Status>ACTIVE</Status><Name>Training</Name></ContactGroup></ContactGroups><Balances><AccountsReceivable><Overdue>234.00</Overdue><Outstanding>234.00</Outstanding></AccountsReceivable><AccountsPayable><Overdue>130.00</Overdue><Outstanding>130.00</Outstanding></AccountsPayable></Balances><BankAccountDetails>10-20-30 987654321</BankAccountDetails><ContactID>362819c9-f285-4d09-ac95-26327863adac</ContactID><Addresses><Address><City>Oaktown</City><AddressType>POBOX</AddressType><Country>United Kingdom</Country><Region>Madeupville</Region><AttentionTo>Club Secretary</AttentionTo><AddressLine2>South Mailing Centre</AddressLine2><AddressLine1>P O Box 3354</AddressLine1><PostalCode>MA12 VL3</PostalCode></Address><Address><City>Ridge Heights</City><AddressType>STREET</AddressType><Country>United Kingdom</Country><Region>Madeupville</Region><AttentionTo>Club Secretary</AttentionTo><AddressLine1>148 Bay Harbour Road</AddressLine1><PostalCode>MA12 VL9</PostalCode></Address></Addresses><EmailAddress>[email protected]</EmailAddress><Phones><Phone><PhoneAreaCode>02</PhoneAreaCode><PhoneNumber>2024418</PhoneNumber><PhoneType>DDI</PhoneType></Phone><Phone><PhoneAreaCode>02</PhoneAreaCode><PhoneNumber>2024455</PhoneNumber><PhoneType>DEFAULT</PhoneType></Phone><Phone><PhoneAreaCode>02</PhoneAreaCode><PhoneNumber>2025566</PhoneNumber><PhoneType>FAX</PhoneType></Phone><Phone><PhoneAreaCode>01</PhoneAreaCode><PhoneNumber>7774455</PhoneNumber><PhoneType>MOBILE</PhoneType></Phone></Phones><AccountsReceivableTaxType>OUTPUT2</AccountsReceivableTaxType><IsCustomer>True</IsCustomer><AccountsPayableTaxType>INPUT2</AccountsPayableTaxType><BatchPayments><BankAccountName>BSA</BankAccountName><BankAccountNumber>10-20-30 987654321</BankAccountNumber><Details>OIT2-UK</Details></BatchPayments><ContactStatus>ACTIVE</ContactStatus></Contact><Date>2013-11-05</Date><SubTotal>450.0</SubTotal><CurrencyCode>GBP</CurrencyCode><InvoiceNumber>I0001</InvoiceNumber><Type>ACCREC</Type><Total>540.0</Total></Invoice>

Note the UpdatedDateUTC field.

Unable to Assign tracking codes to line items when creating invoice

These both fail to work. I was thwarted by #16, but now get the codes returned. Posting them to invoices seems to fail however.
'''
items.append({
'Description': description,
'Quantity': '1.0',
'UnitAmount': price,
'AccountCode': '200',
# 'DiscountRate': '0',
'Tracking': {'TrackingCategory': {'TrackingCategoryID':'xxxxxx-6beb-4f36-a511-0d45f22c107e', 'Name': 'HH - Job', 'TrackingOptionID': 'xxxxxx-7814-4e74-a7cb-ed85f5e857a6'}},
'TaxType': '%s' % tax_type,
})
'''

and

'''
items.append({
'Description': description,
'Quantity': '1.0',
'UnitAmount': price,
'AccountCode': '200',
# 'DiscountRate': '0',
# 'Tracking': '200',
'Tracking': {'TrackingCategory': {'TrackingCategoryID':'xxxxxx-6beb-4f36-a511-0d45f22c107e', 'Name': 'HH - Job', 'Option': 'The Big Client App},},
'TaxType': '%s' % TAX_TYPE,
'''

Support raw filtering

Trying to achieve a mapping from Django filtering to Xero filtering is fairly optimistic, there's a lot of filtering that isn't possible now ... eg. this doesn't work:

contact_invoices = xero.invoices.filter(Contact_ContactID='83ad77d8-48a7-4f77-9146-e6933b7fb63b')

It might be nicer to support raw filtering, e.g:

contact_invoices = xero.invoices.filter(raw='Contact.ContactID=GUID("83ad77d8-48a7-4f77-9146-e6933b7fb63b")')

Exception on "XeroBadRequest" exception class in exceptions.py

Hello, I am using your library and I realised than on the class "XeroBadRequest" in exceptions.py, they may be something extreme that could cause exceptions...

To test it, do this: create an invoice (for instance an invoice) with a fake ContactID that you're sure is not on your database. Then, by the line 23, when the loop is trying to pull the error messages out, it will fail since in certain situations the Xero API is not responding "Elements" on data so it is not possible to use "data['elements']" because it doesn't exist.

I changed the source of the init function to this (just in case it is valid and you want to use it in your code):

self.errors = [err['Message']
    for elem in data.get('Elements', [])
    for err in elem.get('ValidationErrors', [])
] or [data['Message']]

Cheers!

Filters Same Field With 'or'

It's not clear how to do this

Status=="AUTHORISED" OR Status=="PAID"

I tried Status=['AUTHORISED', 'PAID'] as my filter however it didn't work. I do not see how this usecase is handled in code. Thanks!

Files API

I am looking at adding support for the recently released FIles API.

The Files API is a fair bit different to how the Accounting API has worked in the past. For example the Files API is more RESTful. If you were to update and existing folder for example the url would be a POST on ..../Folders/{FolderID}. The Files API also uses the correct HTTP methods for creating (POST) and updating (PUT) so requests to the Files API are the opposite to what is implemented for the accounting endpoints in the wrapper. It is also JSON by default and much more JSON friendly and the current manager.py handles everything as XML.
All in all a lot of the files stuff would contradict with what is currently in manager.py.

How would you suggest I go about adding the Files functionality? Would it be best if the manager.py was left for the 'accounting' aspects of the API and I created another class like a FilesManager.py for access to the Files API? Or something else?

Install Error

When trying to install I received something relating to this.

Using /srv/dev-supportcenter/lib/python2.7/site-packages
Finished processing dependencies for pyxero==0.5.2
Error in atexit._run_exitfuncs:
Traceback (most recent call last):
File "/usr/lib/python2.7/atexit.py", line 24, in _run_exitfuncs
func(_targs, *_kargs)
File "/usr/lib/python2.7/multiprocessing/util.py", line 284, in _exit_function
info('process shutting down')
TypeError: 'NoneType' object is not callable
Error in sys.exitfunc:
Traceback (most recent call last):
File "/usr/lib/python2.7/atexit.py", line 24, in _run_exitfuncs
func(_targs, *_kargs)
File "/usr/lib/python2.7/multiprocessing/util.py", line 284, in _exit_function
info('process shutting down')
TypeError: 'NoneType' object is not callable

Had to import multiprocessing as suggested here to the top of the setup.py file.

https://groups.google.com/forum/#!msg/nose-users/fnJ-kAUbYHQ/ngz3qjdnrioJ

I've found this in a few different python scripts lately

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.