gapipy 2.42.0
Python client for the G Adventures REST API
License: MIT
Keywords: gapipy

G API Python Client
===================

.. image:: https://badge.fury.io/py/gapipy.svg
    :target: http://badge.fury.io/py/gapipy

A client for the G Adventures REST API (https://developers.gadventures.com)

* GitHub Repository: https://github.com/gadventures/gapipy/
* Documentation: http://gapipy.readthedocs.org.
* Free software: MIT License


Quick Start
-----------

.. code-block:: python

   >>> from gapipy import Client
   >>> api = Client(application_key='MY_SECRET_KEY')

   >>> # Get a resource by id
   >>> tour_dossier = api.tour_dossiers.get(24309)
   >>> tour_dossier.product_line
   u'AHEH'
   >>> tour_dossier.departures.count()
   134
   >>> tour_dossier.name
   u'Essential India'
   >>> itinerary = tour_dossier.structured_itineraries[0]
   >>> {day.day: day.summary for day in itinerary.days[:3]}
   {1: u'Arrive at any time. Arrival transfer included through the G Adventures-supported Women on Wheels project.',
   2: u'Take a morning walk through the city with a young adult from the G Adventures-supported New Delhi Streetkids Project. Later, visit Old Delhi, explore the spice markets, and visit Jama Masjid and Connaught Place.',
   3: u"Arrive in Jaipur and explore this gorgeous 'pink city'."}

   >>> # Create a new resource
   >>> booking = api.bookings.create({'currency': 'CAD', 'external_id': 'abc'})

   >>> # Modify an existing resource
   >>> booking.external_id = 'def'
   >>> booking.save()


Since `2.25.0 (2020-01-02)`_

.. code-block:: python

   >>> # since 2.25.0 reference stubs that fail to fetch will return a

   >>> # subclass of requests.HTTPError (See: https://github.com/gadventures/gapipy/pull/119)
   >>> # This can also be done on Query.get by passing a Falsy value for the
   >>> # httperrors_mapped_to_none kwarg.
   >>>
   >>> dep = api.departures.get('404_404', httperrors_mapped_to_none=None)
   ... # omitted stacktrace
   HTTPError: 404 Client Error: {"http_status_code":404,"message":"Not found.","errors":[],"time":"2020-01-02T19:46:07Z","error_id":"gapi_asdf1234"} for url: https://rest.gadventures.com/departures/404_404

   >>> dep = api.departures.get('404404')
   >>> dep.start_address.country
   <Country: BR (stub)>

   >>> # lets have GAPI return a _404_ error here for the country stub `fetch`
   >>> # when we attempt to retrieve the continent attribute

   >>> dep.start_address.country.continent  # reference/stub forces a fetch

   >>> # pre 2.25.0 behaviour
   ... # omitted stacktrace
   AttributeError: 'Country' has no field 'continent' available

   >>> # post 2.25.0 behaviour
   ... # omitted stacktrace
   HTTPError: 404 Client Error: {"http_status_code":404,"message":"Not found.","errors":[],"time":"2020-01-02T19:46:07Z","error_id":"gapi_qwer5678"} for url: https://rest.gadventures.com/countries/BR


Resources
---------

Resource objects are instantiated from python dictionaries created from JSON
data. The fields are parsed and converted to python objects as specified in the
resource class.

A nested resource will only be instantiated when its corresponding attribute is
accessed in the parent resource. These resources may be returned as a ``stub``,
and upon access of an attribute not present, will internally call ``.fetch()``
on the resource to populate it.

A field pointing to the URL for a collection of a child resources will hold a
``Query`` object for that resource. As for nested resources, it will only be
instantiated when it is first accessed.


Queries
-------

A Query for a resource can be used to fetch resources of that type (either a
single instance or an iterator over them, possibly filtered according to  some
conditions). Queries are roughly analogous to Django's QuerySets.

An API client instance has a query object for each available resource
(accessible by an attribute named after the resource name)


Methods on Query objects
========================

All queries support the ``get``, ``create`` and ``options`` methods. The other
methods are only supported for queries whose resources are listable.

``options()``
   Get the options for a single resource

``get(resource_id, [headers={}])``
   Get a single resource; optionally passing in a dictionary of header
   values.

``create(data)``
   Create an instance of the query resource using the given data.

``all([limit=n])``
   Generator over all resources in the current query. If ``limit`` is a
   positive integer ``n``, then only the first ``n`` results will be returned.

   * A ``TypeError`` will be raised if limit is not ``None`` or ``int`` type
   * A ``ValueError`` will be raised if ``limit <= 0``

``filter(field1=value1, [field2=value2, ...])``

``filter(**{"nested.field": "value"})``
   Filter resources on the provided fields and values. Calls to ``filter`` can
   be chained. The method will return a clone of the ``Query`` object and must
   be stored in a separate variable in order to have access to **stacked**
   filters.

``count()``
   Return the number of resources in the current query (by reading the
   ``count`` field on the response returned by requesting the list of
   resources in the current query).


Caching
-------

``gapipy`` can be configured to use a cache to avoid having to send HTTP
requests for resources it has already seen. Cache invalidation is not
automatically handled: it is recommended to listen to G API webhooks_ to purge
resources that are outdated.

.. _webhooks: https://developers.gadventures.com/docs/webhooks.html

By default, ``gapipy`` will use the cached data to instantiate a resource, but
a fresh copy can be fetched from the API by passing ``cached=False`` to
``Query.get``. This has the side-effect of recaching the resource with the
latest data, which makes this a convenient way to refresh cached data.

Caching can be configured through the ``cache_backend`` and ``cache_options``
settings. ``cached_backend`` should be a string of the fully qualified path to
a cache backend, i.e. a subclass of ``gapipy.cache.BaseCache``. A handful of
cache backends are available out of the box:

``gapipy.cache.SimpleCache``
   A simple in-memory cache for single process environments and is not
   thread safe.

``gapipy.cache.RedisCache``
   A key-value cache store using Redis as a backend.

``gapipy.cache.NullCache`` (Default)
   A cache that doesn't cache.

``gapipy.cache.DjangoCache`` (requires Django)
   A cache which uses Django's cache settings for configuration. Requires there
   be a ``gapi`` entry in ``settings.CACHES``.

Since the cache backend is defined by a python module path, you are free to use
a cache backend that is defined outside of this project.


Connection Pooling
------------------

We use the ``requests`` library, and you can take advantage of the provided
connection pooling options by passing in a ``'connection_pool_options'`` dict
to your client.

Values inside the ``'connection_pool_options'`` dict of interest are as
follows:

* Set ``enable`` to ``True`` to enable pooling. Defaults to ``False``.
* Use ``number`` to set the number of connection pools to cache.
  Defaults to 10.
* Use ``maxsize`` to set the max number of connections in each pool.
  Defaults to 10.
* Set ``block`` to ``True`` if the connection pool should block and wait
  for a connection to be released when it has reached ``maxsize``. If
  ``False`` and the pool is already at ``maxsize`` a new connection will
  be created without blocking, but it will not be saved once it is used.
  Defaults to ``False``.

See also:
---------

* http://www.python-requests.org/en/latest/api/#requests.adapters.HTTPAdapter
* http://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.connectionpool


Dependencies
------------

The only dependency needed to use the client is requests_.

.. _requests: http://python-requests.org


Testing
-------

Running tests is pretty simple. We use `nose` as the test runner. You can
install all requirements for testing with the following::

   $ pip install -r requirements-testing.txt

Once installed, run unit tests with::

   $ nosetests -A integration!=1

Otherwise, you'll want to include a GAPI Application Key so the integration
tests can successfully hit the API::

   $ export GAPI_APPLICATION_KEY=MY_SECRET_KEY; nosetests

In addition to running the test suite against your local Python interpreter, you
can run tests using `Tox <http://tox.testrun.org>`_. Tox allows the test suite
to be run against multiple environments, or in this case, multiple versions of
Python. Install and run the ``tox`` command from any place in the gapipy source
tree. You'll want to export your G API application key as well::

   $ export GAPI_APPLICATION_KEY=MY_SECRET_KEY
   $ pip install tox
   $ tox

Tox will attempt to run against all environments defined in the ``tox.ini``. It
is recommended to use a tool like `pyenv <https://github.com/yyuu/pyenv>`_ to
ensure you have multiple versions of Python available on your machine for Tox to
use.


Fields
------

* ``_model_fields`` represent dictionary fields.

.. note::

   * ``_model_fields = [('address', Address)]`` AND
   * ``Address`` subclasses ``BaseModel``

.. code-block:: python

   {
      "address": {
         "street": "19 Charlotte St",
         "city": "Toronto",
         "state": {
            "id": "CA-ON",
            "href": "https://rest.gadventures.com/states/CA-ON",
            "name": "Ontario"
         },
         "country": {
            "id": "CA",
            "href": "https://rest.gadventures.com/countries/CA",
            "name": "Canada"
         },
         "postal_zip": "M5V 2H5"
      }
   }


* ``_model_collection_fields`` represent a list of dictionary fields.

.. note::

   * ``_model_collection_fields = [('emails', AgencyEmail),]`` AND
   * ``AgencyEmail`` subclasses ``BaseModel``

.. code-block:: python

   {
      "emails": [
         {
            "type": "ALLOCATIONS_RELEASE",
            "address": "g@gadventures.com"
         },
         {
            "type": "ALLOCATIONS_RELEASE",
            "address": "g2@gadventures.com"
         }
      ]
   }

* ``_resource_fields`` refer to another ``Resource``




Contributing
============

.. note:: Ensure a Python 2 environment

0. Clone the project

   .. code-block:: sh

      $ git clone git@github.com:gadventures/gapipy


1. Run ``pip install -r requirements-dev.txt`` to setup dev dependencies.


2. Always make your changes in a branch and to submit a PR.

   .. code-block:: sh

      $ git checkout master
      $ git pull
      $ git checkout -b feature-branch-name
      $ git push origin feature-branch-name


3. Once the PR has been accepted/merged into the ``master`` branch, follow
   these steps.

   .. code-block:: sh

      $ cd /path/to/gapipy
      $ git checkout master
      $ git pull origin master


**Modify the following files:**

* Update **gapipy/__init__.py**

   * increment the ``__version__`` variable

   .. note::

      * style ``major.minor.patch``
      * update ``patch`` when adding new fields, fixing bugs introduced by a
        minor release.
      * update ``minor`` if there is some breaking change such as adding a new
        resource, removing fields, adding new behaviour.
      * update ``major`` when we switch to ``Python 3`` only support.
      * See `semver.org <https://semver.org>`_ for more information.

* Update **HISTORY.rst**

   * update this file with the new ``version`` & ``date``
   * Add some brief notes describing the changes.


4. Use ``make dist`` to check the generated long_description rST file is valid.

   .. note::

      * ignore ``warning: no previously-included files matching`` messages.
      * as long as you get a ``Checking dist/gapipy-a.b.c.tar.gz: PASSED``
        message, you are good!
      * If not, fix the errors as dictated in the output, and repeat.

   Example output when running ``make dist``:

   .. code-block:: sh

      $ make dist
      warning: no previously-included files matching '*' found under directory 'tests'
      warning: no previously-included files matching '__pycache__' found under directory '*'
      warning: no previously-included files matching '.eggs' found under directory '*'
      warning: no previously-included files matching '*.py[co]' found under directory '*'
      total 123
      -rw-r--r--  1 user  group  76276  5 Feb 02:53 gapipy-a.b.c.tar.gz
      Checking dist/gapipy-a.b.c.tar.gz: PASSED


5. Push the new *Release* commit

   * Use **Release a.b.c (YYYY-MM-DD)** format for the commit title. Optionally
     add a description that matches the changes made to **HISTORY.rst**.


6. Create a release on github with the following description (This will be
   tagged to the ``Release`` commit and not the PR/change commit)

   .. code-block:: md

      # Release a.b.c (YYYY-MM-DD)

      PR: #123

      A brief description describing the changes
      * bullet points
      * make for easy reading


7. Release!

      $ make release

Thanks for helping!
-------------------



History
=======

2.42.0 (2026-02-19)
-------------------

* Adds ``date_last_modified`` field to the ``BookingCompany`` resource.
* See `PR #149`_ for more details.

.. _`PR #149`: https://github.com/gadventures/gapipy/pull/149


2.41.0 (2025-12-02)
-------------------

* Add `gapipy.exceptions.TimeoutError`, which can be raised when the optional
  `timeout` parameter is passed to `Query.get`. Without providing the `timeout`
  parameter, the default behaviour remains unchanged, where a `requests`
  Timeout exception is raised.
* See `PR #146`_ for more details.

  .. code-block:: python

    from gapipy import Client

    gapi = Client(application_key="your_api_key")
    try:
        departure_service = gapi.departure_services.get(123456, timeout=1)
    except gapi.exceptions.TimeoutError:
        # handle exception
    else:
        ... # success

.. _`PR #146`: https://github.com/gadventures/gapipy/pull/146


2.40.0 (2025-11-06)
-------------------

* HOTFIX for `2.39.0 (2025-08-21) (Yanked)`_: Reverts the removal of the
  `self._raw_data = deepcopy(data)` in ``BaseModel._fill_fields``. This is
  necessary to ensure that the ``_raw_data`` attribute is updated with new data
  returned as a result of the request made in ``Resource.save()``. This bug was
  introduced in `PR #145`_.
* All other changes in 2.39.0 will remain and 2.39.0 will be yanked from PyPI.
* See `PR #147`_ for more details.

.. _`PR #147`: https://github.com/gadventures/gapipy/pull/147


2.39.0 (2025-08-21) (Yanked)
----------------------------

* Remove ``costs`` and ``has_costs`` fields from the ``AccommodationDossier``
  and ``ActivityDossier`` resources.
* Moves the ``AccommodationDossier.features`` field from the as-is-fields to
  the model-collection-fields as references to the ``DossierFeature`` resource.
* Expose the ``primary_country`` field on the ``ActivityDossier`` resource.
  This references the ``Country`` resource.
* See `PR #145`_ for more details.

.. _`PR #145`: https://github.com/gadventures/gapipy/pull/145


2.38.0 (2025-05-29)
-------------------

* Add new resources for "room upgrade" and "regional connector" products:
  ``room_upgrades``, ``room_upgrade_services``, ``regional_connectors``, and
  ``regional_connectors_services``. See `PR #143`_ for more details.

.. _`PR #143`: https://github.com/gadventures/gapipy/pull/143


2.37.0 (2025-02-19)
-------------------

* Add ``abta_number`` field to the ``Agency`` resource. This field is a string
  that represents the ABTA number of the agency. See `PR #142`_ for more
  details.

.. _`PR #142`: https://github.com/gadventures/gapipy/pull/142


2.36.0 (2024-05-30)
-------------------

* Add ``contact_us`` field to the ``AgenchChain`` resource. This field can be
  ``None``, however should a value be present, it will be an object with three
  accessible attributes: ``email``, ``phone_number``, and ``website_url``. See
  the `PR #141`_ for more details.

.. _`PR #141`: https://github.com/gadventures/gapipy/pull/141


2.35.0 (2022-04-18)
-------------------

* Add new ``Client`` configuration value that will raise an error when an empty
  partial update (PATCH) payload is computed by gapipy. See `Issue #136`_ and
  the corresponding `PR #137`_ for more details.

* The new Client configuration kwarg is ``raise_on_empty_update``, whose
  default value is ``False``, and can also be set by passing it as an
  environment variable ``GAPI_CLIENT_RAISE_ON_EMPTY_UPDATE``. If this config
  value is set, then a call to ``Resource.save`` with ``partial=True`` will
  raise the new ``EmptyPartialUpdateError`` if an empty payload is computed.

  .. code-block:: python

    from gapipy import Client

    gapi = Client(application_key="your_api_key", raise_on_empty_update=True)
    departure_service = gapi.departure_services.get(123456)

    # we've changed nothing and are calling a partial save (PATCH)
    #
    # NOTE: the new EmptyPartialUpdateError will be raised here
    departure_service.save(partial=True)

.. _`Issue #136`: https://github.com/gadventures/gapipy/issues/136
.. _`PR #137`: https://github.com/gadventures/gapipy/pull/137


2.34.0 (2021-08-20)
-------------------

* Add ``travel_ready_policy`` model field to the ``Departure`` resource.
* More details can be found in our developer documentation.
  c.f. `Departure travel-ready-policy`_

.. _`Departure travel-ready-policy`: https://developers.gadventures.com/docs/departure.html#travel-ready-policy


2.33.0 (2021-07-06)
-------------------

* Add ``online_preferences`` field to the ``Agency Chain`` resource.


2.32.0 (2021-06-18)
-------------------

* Make ``future`` requirement more flexible. See `PR #134`_ for more details.

.. _`PR #134`: https://github.com/gadventures/gapipy/pull/134


2.31.1 (2021-05-14)
-------------------

* Initialize the ``DjangoCache`` via the ``BaseCache`` which exposes the
  ``default_timeout`` attribute to the class. Prior to this change, when using
  the ``DjangoCache``, items would persist forever as no timeout would be set
  on the entries. See `PR #133`_ for more details.

.. note:: ``DjangoCache`` was introduced in `2.30.0 (2021-02-08)`_

.. _`PR #133`: https://github.com/gadventures/gapipy/pull/133


2.31.0 (2021-03-02)
-------------------

* Introduce ``gapipy.constants`` module that holds common constants. See
  `PR #132`_ for more details.

* Reintroduce the ability to enable old behaviour (pre `2.25.0 (2020-01-02)`_)
  for ``Resource.fetch``. It adds an optional ``httperrors_mapped_to_none``
  parameter to the method (default ``None``), where if a list of HTTP Status
  codes is provided instead, will silently consume errors mapped to those
  status codes and return a ``None`` value instead of raising the HTTPError.
  See `PR #131`_ for more details.

.. _`PR #131`: https://github.com/gadventures/gapipy/pull/131
.. _`PR #132`: https://github.com/gadventures/gapipy/pull/132


2.30.1 (2021-02-08)
-------------------

* Fix for `2.30.0 (2021-02-08)`_ Adds a guard against configuring Django
  settings again as per the `Django settings docs`_. See `PR #130`_ for more
  details.

.. _`Django settings docs`: https://docs.djangoproject.com/en/3.1/topics/settings/#either-configure-or-django-settings-module-is-required
.. _`PR #130`: https://github.com/gadventures/gapipy/pull/130


2.30.0 (2021-02-08)
-------------------

* Adds a new cache backend; ``gapipy.cache.DjangoCache``. It requires ``Django``
  and a ``gapi`` entry in ``settings.CACHES``. See `PR #129`_ for more details.

.. _`PR #129`: https://github.com/gadventures/gapipy/pull/129/

**Usage:**

* Set the ``GAPI_CACHE_BACKEND`` Env varible to ``gapipy.cache.DjangoCache``.

OR

.. code-block:: python

   from gapipy import Client

   gapi = Client(
      application_key="live_your-secret-gapi-key",
      cache_backend="gapipy.cache.DjangoCache",
   )


2.29.0 (2021-02-05)
-------------------

* Adds ``Departure.relationships`` field via ``DepartureRelationship`` model
* Adds ``TourDossier.relationships`` field via ``TourDossierRelationship``
  model

.. warning:: BREAKING!

* Moves the ``gapipy.resources.tour.itinerary.ValidDuringRange`` class over to
  its own file ``gapipy.models.valid_duraing_range.ValidDuringRange``
  so that it can be reused by the ``TourDossierRelationship`` model. Any code
  importing the class directly will need to change the import path:

   .. code-block:: python

      # before
      from gapipy.resources.tour.itinerary.ValidDuringRange

      # now
      from gapipy.models import ValidDuringRange

* See `PR #128`_ for more details.

.. _`PR #128`: https://github.com/gadventures/gapipy/pull/128/


2.28.0 (2020-11-23)
-------------------

* Add a new ``Client`` config option, ``global_http_headers``, a dict of HTTP
  headers to add to each request made with that client.

  This is similar to the ``headers=`` kwargs available when making ``get`` and
  ``create`` calls, except that the ``global_http_headers`` set on a client
  will apply on *every request* made by that client instance.


2.27.0 (2020-05-26)
-------------------

.. warning:: BREAKING!

* Make ``Customer.nationality`` a *resource field*. This allows attribute style
  access to the field values, whereas before they needed to be accessed using
  dictionary accessor (``d["key"]``) syntax.

.. code-block:: python

   # before
   >>> api.customers.get(123456).nationality["name"]
   u'Canadian'

   # now
   >>> api.customers.get(123456).nationality.name
   u'Canadian'


2.26.4 (2020-04-28)
-------------------

* Fix `2.26.3 (2020-04-28) (Yanked)`_: Add missing ``CONTRIBUTING.rst`` to the
  manifest.

.. note:: Discovered when attempting to install ``gapipy`` via ``pip``.


2.26.3 (2020-04-28) (Yanked)
----------------------------

* Fix py2 & py3 compatibility for ``urlparse``


2.26.2 (2020-04-20)
-------------------

* Fix for `2.26.1 (2020-04-20)`_ and `Issue #113`_.

   * See `PR #125`_.
   * Remove the ``_set_resource_collection_field`` method in ``TourDossier``
   * Introducing the ``_Parent`` namedtuple in `PR #123`_.
     broke being able to Query-chain from Tour-Dossiers to departures
   * Buggy behaviour fixed from `2.26.1 (2020-04-20)`_

   .. code-block:: python

      >>> from gapipy import Client
      >>> api = Client(application_key='MY_SECRET_KEY')

      >>> api.tour_dossiers(24309).departures.count()
      # AttributeError: 'tuple' object has no attribute 'uri'

.. _`PR #125`: https://github.com/gadventures/gapipy/pull/125


2.26.1 (2020-04-20)
-------------------

* Fix for `2.26.0 (2020-04-14)`_ and `Issue #113`_.

   * Calls to ``APIRequestor.list_raw`` will use initialised its parameters,
     unless the URI provides its own.
   * See `PR #123`_.

* Add the ability to define the ``max_retries`` values on the requestor.

   * New ``env`` value ``GAPI_CLIENT_MAX_RETRIES``.
   * The default value will be ``0``, and if provided will override the ``retry``
     value on the ``requests.Session``.
   * This change will also always initialize a ``requests.Session`` value on
     initialisation of the ``gapipy.Client``.
   * See `PR #124`_.

* Add ``variation_id`` field to the ``Image`` resource.

   * See `Commit edc8d9b`_.

* Update the ``ActivityDossier`` and ``AccommodationDossier`` resources.

   * Remove the ``is_prepaid`` field.
   * Adds the ``has_costs`` field.
   * See `Commit bd35531`_. 

.. _`Issue #113`: https://github.com/gadventures/gapipy/issues/113
.. _`PR #123`: https://github.com/gadventures/gapipy/pull/123
.. _`PR #124`: https://github.com/gadventures/gapipy/pull/124
.. _`Commit edc8d9b`: https://github.com/gadventures/gapipy/commit/edc8d9b
.. _`Commit bd35531`: https://github.com/gadventures/gapipy/commit/bd35531


2.26.0 (2020-04-14)
-------------------

.. warning:: BREAKING!

* The ``Query.filter`` method will return a clone/copy of itself. This will
  preserve the state of ``filters`` on the original Query object.
* The ``Query.all`` method will **not** clear the filters after returning.
* The ``Query.all`` method will return a ``TypeError`` if a type other than
  an ``int`` is passed to the ``limit`` argument.
* The ``Query.count`` method will **not** clear the filters after returning.
* See `PR #121`_ for more details.

New behaviour with the ``Query.filter`` method:

.. code-block:: python

   >>> from gapipy import Client
   >>> api = Client(application_key='MY_SECRET_KEY')

   # create a filter on the departures
   >>> query = api.departures.filter(**{"tour_dossier.id": "24309"})
   >>> query.count()
   494

   # we preserve the filter status of the current query
   >>> query.filter(**{"availability.status": "AVAILABLE"}).count()
   80

   >>> query.count()
   494

* The ``AgencyChain.agencies`` attribute returns a list of ``Agency`` objects.
  See `Commit f34afd52`_.

.. _`PR #121`: https://github.com/gadventures/gapipy/pull/121
.. _`Commit f34afd52`: https://github.com/gadventures/gapipy/commit/f34afd52


2.25.1 (2020-01-02)
-------------------

* Improve contribution instructions to check long_description rST file in dist
* Dev Requirement updates:

   * Add ``readme_renderer==24.0``
   * Add ``twine==1.15.0`` for ``twine check`` command


2.25.0 (2020-01-02)
-------------------

* Failing to fetch inlined Resource (from Stubs) will raise the underlying
  requests.HTTPError instead of AttributeError resulting from a ``None``.
* Adds ``httperrors_mapped_to_none`` kwarg to ``gapipy.query.Query.get``
  with default value ``gapipy.query.HTTPERRORS_MAPPED_TO_NONE``
* Modifies ``gapipy.resources.base.Resource.fetch`` to
  pass ``httperrors_mapped_to_none=None`` to ``Query.get``
* This ensures that any underlying ``requests.HTTPError`` from ``Query.get``
  is bubbled up to the caller. It is most prevalent when reference Resource stubs
  fail to be retrieved from the G API. Prior to this change ``Resource.fetch``
  would return a ``None`` value resulting in an ``AttributeError``. Now, if the
  stub fails to fetch due to an HTTPError, that will be raised instead


2.24.3 (2019-12-12)
-------------------

* Exclude the ``tests`` package from the package distribution


2.24.2 (2019-12-12)
-------------------

* Adds the ``compute_request_signature`` and ``compute_webhook_validation_key``
  utility methods. See `PR #122`_.

.. _`PR #122`: https://github.com/gadventures/gapipy/pull/122


2.24.1 (2019-12-12)
-------------------

* Add ``slug`` field to ``TourDossier`` resource. See `PR #120`_.

.. _`PR #120`: https://github.com/gadventures/gapipy/pull/120


2.24.0 (2019-11-05)
-------------------

* Add missing/new fields to the following resources. See `PR #117`_.

   * AccommodationDossier: ``categories``, ``suggested_dossiers``, ``visited_countries``, ``visited_cities``
   * ActivityDossier: ``suggested_dossiers``, ``visited_countries``, ``visited_cities``
   * Departure: ``local_payments``
   * Itinerary: ``publish_state``

* Add ``continent`` and ``place`` references to the ``Countries`` resource. See
  `PR #115`_.
* Accept ``additional_headers`` optional kwarg on ``create``. See `PR #114`_.

.. _`PR #114`: https://github.com/gadventures/gapipy/pull/114
.. _`PR #115`: https://github.com/gadventures/gapipy/pull/115
.. _`PR #117`: https://github.com/gadventures/gapipy/pull/117


2.23.0 (2019-11-04)
-------------------

* Remove deprecated ``tour_dossiers.itineraries`` field and related code


2.22.0 (2019-10-10)
-------------------

* Add ``booking_company`` field to ``Booking`` resource


2.21.0 (2019-04-09)
-------------------

* Add ``ripple_score`` to ``Itinerary`` resource


2.20.1 (2019-02-20)
-------------------

* HISTORY.rst doc fixes


2.20.0 (2019-02-20)
-------------------

* Add ``Requirement`` and ``RequirementSet`` resources
* Move ``Checkin`` resource to the ``resources.booking`` module
* The ``Query`` object will resolve to use the ``href`` value when
  returning the iterator to fetch ``all`` of some resource. This is
  needed because ``bookings/123456/requirements`` actually returns a list
  of ``RequirementSet`` resources
* See `Release tag 2.20.0`_ for more details.

.. _`Release tag 2.20.0`: https://github.com/gadventures/gapipy/releases/tag/2.20.0


2.19.4 (2019-02-14)
-------------------

* Add ``get_category_name`` helper method to ``TourDossier`` resource


2.19.3 (2019-02-12)
-------------------

* Attempt to fix rST formatting of ``README`` and ``HISTORY`` on pypi


2.19.2 (2019-02-12)
-------------------

* Become agnostic between redis ``2.x.x`` && ``3.x.x`` versions

   * the ``setex`` method argument order changes between the major versions


2.19.1 (2019-02-12)
-------------------

.. note:: HotFix for `2.19.0 (2019-02-12)`_.

* adds ``requirements.txt`` file to the distribution ``MANIFEST``


2.19.0 (2019-02-12)
-------------------

* Add ``booking_companies`` field to ``Itinerary`` resource
* Pin our requirement/dependency versions

   * pin ``future == 0.16.0``
   * pin ``requests >= 2.18.4, < 3.0.0``
   * read ``setup.py`` requirements from ``requirements.txt``


2.18.1 (2019-02-07)
-------------------

* Add ``customers`` nested resource to ``bookings``


2.18.0 (2018-12-14)
-------------------

* Add ``merchandise`` resource
* Add ``merchandise_services`` resources


2.17.0 (2018-11-12)
-------------------

* Add ``membership_programs`` field to the ``Customer`` resource


2.16.0 (2018-11-07)
-------------------

* Completely remove the deprecated ``add_ons`` field from the Departure resource
* Add missing fields to various Dossier resources

   * AccommodationDossier: ``flags``, ``is_prepaid``, ``service_time``, ``show_on_reservation_sheet``
   * ActivityDossier: ``is_prepaid``, ``service_time``, ``show_on_reservation_sheet``
   * CountryDossier: ``flags``
   * PlaceDossier: ``flags``
   * TransportDossier: ``flags``

* Add ``valid_during_ranges`` list field to the Itinerary resource. This field is
  a list field of the newly added ``ValidDuringRange`` model (described below)
* Add ``ValidDuringRange`` model. It consists of two date fields, ``start_date``,
  and ``end_date``. It also provides a number of convenience methods to determine
  if the date range provided is valid, or relative to some date.

   * ``is_expired``: Is it expired relative to ``datetime.date.today``
   * ``is_valid_today``: Is it valid relative to ``datetime.date.today``
   * ``is_valid_during_range``: Is it valid for some give start/end date range
   * ``is_valid_on_or_after_date``: Is it valid on or after some date
   * ``is_valid_on_or_before_date``: Is it valid on or before some date
   * ``is_valid_on_date``: Is it valid on some date
   * ``is_valid_sometime``: Is it valid at all


2.15.0 (2018-10-10)
-------------------

* Add ``country`` reference to ``Nationality`` resource.
* Moved ``resources/bookings/nationality.py`` to ``resources/geo/*``.


2.14.6 (2018-08-01)
-------------------

* Check for presence of ``id`` field directly in the Resource ``__dict__`` in
  order to prevent a chicken/egg situation when attempting to ``save``. This is
  needed due to the change introduced in 2.14.4, where we explicitly raise an
  AttributeError when trying to access the ``id`` attribute.
* Added ``service_code`` field for Activty & Accommodation Dossier resources.


2.14.5 (2018-08-01)
-------------------

* deleted


2.14.4 (2018-07-13)
-------------------

* Raise an ``AttributeError`` when trying to access ``id`` on
  ``Resource.__getattr__``.
* Don't send duplicate params when paginating through list results.
* Implement ``first()`` method for ``Query``.

2.14.3 (2018-05-29)
-------------------

* Expose Linked Bookings via the API.

2.14.1 (2018-05-15)
-------------------

* Add ``booking_companies`` field to Agency resource.
* Remove ``bookings`` field from Agency resource.
* Add ``requirements`` as_is field to Departure Service resource.
* Add ``policy_emergency_phone_number`` field to Insurance Service resource.


2.14.0 (2018-05-15)
-------------------

* Remove deprecated ``add_ons`` field from ``Departure`` resource.
* Add ``costs`` field to ``Accommodation`` & ``ActivityDossier`` resources.


2.13.0 (2018-03-31)
-------------------

* Add ``meal_budgets`` list field to ``CountryDossier`` resource.
* Add ``publish_state`` field to ``DossierFeatures`` resource.


2.12.0 (2018-02-14)
-------------------

* Add optional ``headers`` parameter to Query.get to allow HTTP-Headers to be
  passed. e.g. ``client.<resource>.get(1234, headers={'A':'a'})``. See
  `PR #91`_.
* Add ``preferred_display_name`` field to ``Agency`` resource. See `PR #92`_.
* Add ``booking_companies`` array field to all Product-type resources. See
  `PR #93`_.

   * Accommodation
   * Activity
   * AgencyChain
   * Departure
   * SingleSupplement
   * TourDossier
   * Transport

.. _`PR #91`: https://github.com/gadventures/gapipy/pull/91
.. _`PR #92`: https://github.com/gadventures/gapipy/pull/92
.. _`PR #93`: https://github.com/gadventures/gapipy/pull/93


2.11.4 (2018-01-29)
-------------------

* Add ``agency_chain`` field to ``Booking`` resource
* Add ``id`` field as part of the ``DossierDetail`` model See `PR #89`_.
* Add ``agency_chains`` field to the ``Agency`` resource. See `PR #90`_.
* See `Release tag 2.11.3`_ for more details.

.. _`PR #89`: https://github.com/gadventures/gapipy/pull/89
.. _`PR #90`: https://github.com/gadventures/gapipy/pull/90
.. _`Release tag 2.11.3`: https://github.com/gadventures/gapipy/releases/tag/2.11.3


2.11.0 (2017-12-18)
-------------------

* The ``Customer.address`` field uses the ``Address`` model, and is no longer a
  dict.
* Passing in ``uuid=True`` to ``Client`` kwargs enables ``uuid`` generation
  for every request.


2.10.0 (2017-12-01)
-------------------

* Add the ``amount_pending`` field to the ``Booking`` resource
* The ``PricePromotion`` model extends from the ``Promotion`` resource (PR/85)
* Update the ``Agent`` class to use BaseModel classes for the ``role``
  and ``phone_numbers`` fields.
* see `Release tag 2.10.0`_ for more details.

.. _`Release tag 2.10.0`: https://github.com/gadventures/gapipy/releases/tag/2.10.0


2.9.3 (2017-11-23)
------------------

.. note:: We have skipped Release ``2.9.2`` due to pypi upload issues.

* Expose ``requirement_set`` for ``departure_services`` & ``activity_services``.


2.9.1 (2017-11-22)
------------------

.. note:: * We have skipped Release ``2.9.0`` due to pypi upload issues.

* Adds the ``options`` method on the Resource Query object. See
  `Release tag 2.9.1`_ for more details.

.. _`Release tag 2.9.1`: https://github.com/gadventures/gapipy/releases/tag/2.9.1


2.8.2 (2017-11-14)
------------------

* Adds fields ``sale_start_datetime`` and ``sale_finish_datetime`` to the
  Promotion resource. The fields mark the start/finish date-time values
  for when a Promotion is applicable. The values represented are in UTC.


2.8.1 (2017-10-25)
------------------

* Add new fields to the ``Agency`` and ``AgencyChain`` resources


2.8.0 (2017-10-23)
------------------

* This release adds a behaviour change to the ``.all()`` method on resource
  Query objects. Prior to this release, the base Resource Query object would
  retain any previously added ``filter`` values, and be used in subsequent
  calls. Now the underlying filters are reset after a ``<resource>.all()`` call
  is made.

  See `Issue #76`_ and `PR #77`_ for details and the resulting fix. 

* Adds missing fields to the Agency and Flight Service resources (PR/78)

.. _`Issue #76`: https://github.com/gadventures/gapipy/issues/76
.. _`PR #77`: https://github.com/gadventures/gapipy/pull/77


2.7.6 (2017-10-04)
------------------

* Add ``agency`` field to ``Booking`` resource.


2.7.5 (2017-09-25)
------------------

* Add test fix for Accommodation. It is a listable resource as of ``2.7.4``
* Add regression test for departures.addon.product model
  * Ensure Addon's are instantiated to the correct underlying model.
  * Prior to this release, all Addon.product resources were instantiated as
  ``Accommodation``.


2.7.4 (2017-09-20)
------------------

* Add ``videos``, ``images``, and ``categories`` to ``Activity``, ``Transport``,
  ``Place``, and ``Accommodation Dossier`` resources.
* Add ``flags`` to Itinerary resource
* Add list view of ``Accommodations`` resource


2.7.3 (2017-09-06)
------------------

* Add ``type`` field to ``AgencyDocument`` model
* Add ``structured_itinerary`` model collection field to ``Departure`` resource


2.7.2 (2017-08-18)
------------------

* Fix flight_status Reference value in FlightService resource


2.7.1 (2017-08-18)
------------------

* Fix: remove FlightStatus import reference for FlightService resource
* Add fields (fixes two broken Resource tests)

  * Add ``href`` field for ``checkins`` resource
  * Add ``date_cancelled`` field for ``departures`` resource

* Fix broken ``UpdateCreateResource`` tests


2.7.0 (2017-08-18)
------------------

* Remove ``flight_statuses`` and ``flight_segments`` resources.


2.6.2 (2017-08-11)
------------------

* Version bump


2.6.1 (2017-08-11)
------------------

* Adds a Deprecation warning when using the ``tours`` resource.


2.6.0 (2017-08-11)
------------------

* Fixed `Issue #65`_: only
  write data into the local cache after a fetch from the API, do not write data
  into the local cache when fetching from the local cache.

.. _`Issue #65`: https://github.com/gadventures/gapipy/issues/65


2.5.2 (2017-04-26)
------------------

* Added ``future`` dependency to setup.py


2.5.1 (2017-02-08)
------------------

* Fixed an issue in which modifying a nested dictionary caused gapipy to not
  identify a change in the data.
* Added ``tox.ini`` for testing across Python platforms.
* Capture ``403`` Status Codes as a ``None`` object.

2.5.0 (2017-01-20)
------------------

* Provided Python 3 functionality (still Python 2 compatible)
* Removed Python 2 only tests
* Installed ``future`` module for smooth Python 2 to Python 3 migration
* Remove ``DictToModel`` class and the associated tests
* Add ``Dossier`` Resource(s)
* Minor field updates to: ``Customer``, ``InsuranceService``,
  ``DepartureService``, ``Booking``, ``FlightStatus``, ``State``


2.4.9 (2016-11-22)
------------------

* Fixed a bug with internal ``_get_uri`` function.


2.4.8 (2016-11-11)
------------------

* Adjusted ``Checkin`` resource to meet updated spec.


2.4.7 (2016-10-25)
------------------

* Added ``Checkin`` resource.


2.4.6 (2016-10-19)
------------------

* Fix broken ``Duration`` init in ``ActivityDossier`` (likely broke due to
  changes that happened in 2.0.0)


2.4.5 (2016-10-13)
------------------

* Added ``Image`` resource definition and put it to use in ``Itinerary`` and,
  ``PlaceDossier``


2.4.4 (2016-09-09)
------------------

* Added ``date_last_modified`` and ``date_created`` to ``Promotion``.


2.4.3 (2016-09-06)
------------------

* Added ``gender`` to ``Customer``.
* Added ``places_of_interest`` to ``Place``.


2.4.2 (2016-07-08)
------------------

* Added ``departure`` reference to ``DepartureComponent``


2.4.1 (2016-07-06)
------------------

* Removed use of ``.iteritems`` wherever present in favour of ``.items``
* Added ``features`` representation to ``ActivityDossier`` and,
  ``TransportDossier``


2.4.0 (2016-06-29)
------------------

* Added ``CountryDossier`` resource.


2.3.0 (2016-06-28)
------------------

* Added ``DossierSegment`` resource.
* Added ``ServiceLevel`` resource.


2.2.2 (2016-06-08)
------------------

* Added day ``label`` field to the ``Itinerary`` resource.


2.2.1 (2016-06-06)
------------------

* Added ``audience`` field to the ``Document`` resource.


2.2.0 (2016-05-17)
------------------

* Added ``transactional_email``, and ``emails`` to ``Agency`` resource.


2.1.2 (2016-05-17)
------------------

* Added ``audience`` to ``Invoice`` resource.


2.1.1 (2016-04-29)
------------------

* Removed invalid field, ``email`` from ``AgencyChain``


2.1.0 (2016-04-25)
------------------

* Added new resource, ``AgencyChain``


2.0.0 (2016-03-11)
------------------

The global reference to the last