Skip to content

Commit

Permalink
More docs (#304)
Browse files Browse the repository at this point in the history
* Correct doc8 string length

* Cache status and cache engines report added

* Exceptions and cache object reference to docs

* closes #107 show_many objects note

* Contribution guide should be available offline

* Fix static files warning, copyright and year

* requirments and setup.py dependencies sync and version update
  • Loading branch information
insspb authored and facetoe committed Mar 8, 2019
1 parent 52513a9 commit c7e84ce
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 62 deletions.
64 changes: 64 additions & 0 deletions CONTRIBUTING.md
@@ -0,0 +1,64 @@
# Contributors Guide

Looking to hack on Zenpy? Awesome! In this guide I will explain how things hang together to (hopefully) get you started.

## Zenpy Concepts and Terminology

There are few concepts that go together to make up the wrapper. If they make sense, it should be pretty easy to change things.

### Endpoints

Endpoints are callable classes that know how to take some input and return as output the correct path and parameters to query the Zendesk API with. For example, if the ticket endpoint is passed a list of ids to delete, it would output something along the lines of `tickets/destroy_many.json?ids=1,2,3,4,5`. That's all it needs to do.

The `zenpy/lib/endpoint.py` file contains the factory class `EndpointFactory`. This encapsulates all the endpoints and structures them to mirror the structure of Zendesk's API.

### Apis

An `Api` in Zenpy is class that knows how to perform operations on a particular Zendesk endpoint or object. For example, the `TicketApi` knows how to manipulate `Tickets`, and exposes methods such as `merge` for merging `Tickets`.

All the various `Api` classes are encapsulated and exposed to the user through the `Zenpy` class located in `zenpy/__init__.py`.

### Request Handlers

A RequestHandler knows how to make a POST, PUT or DELETE request to Zendesk with the correct format. There are several generic RequestHandlers that handle most cases, however it is occasionally necessary to write new ones to handle edge cases/inconsistencies in the expected format (especially for the ChatApi).

### Response Handlers

A `ResponseHandler` knows how to identify a response that it can handle, and also how to deserealize it from JSON and return the correct type to the user. When a response is returned from Zendesk, each ResponseHandler is tried in order and the first one that matches is executed. ResponseHandlers should always be ordered from most specific to most generic.

### Api Objects

The Zendesk API contains many objects. I am way too lazy to write the code for them all, so I wrote a tool to generate Python classes from JSON instead. It uses Jinja templating and can be found in `tools`. It can be executed from the `tools` directory as follows (requires Python3):

```bash
./gen_classes.py --spec-path ../specification/ --doc-json doc_dict.json -o ../zenpy/lib/
```

Creating a new object is as simple as creating a file in the `zenpy/specification` directory and executing the tool. Once that's done, it will also need to be added to the relevant mapping class in `zenpy/lib/mapping.py`.

For the objects themselves, the idea is that any attribute of an object that can be presented in a more user friendly manner should be converted before being returned. So for example strings representing time should be presented as `datetime` objects, id's for linked objects should be fetched and deserialized and responses that involve pagination should be exposed as generators.

### Debugging

During your work on project contribution or your own project it may be useful to track `zenpy` requests and cache use. Some logging implemented in `zenpy`, so you can switch it on. To do it add this to you application after `zenpy_client` init:

```python
import logging, sys
log = logging.getLogger()
log.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(levelname)s - %(message)s')
ch.setFormatter(formatter)
log.addHandler(ch)
logging.getLogger("requests").setLevel(logging.WARNING)
```

## Putting it all together.

The general flow of execution for a POST, PUT, DELETE action is as follows:

Api -> RequestHandler -> Endpoint -> HTTP METHOD -> ResponseHandler -> (ApiObject or ResultGenerator) returned to user. There are a few steps in between like checking the response, caching objects etc, but that's a general idea.

For a GET request, it is as above except the caches are first checked and a request is only generated if the object is not present.

Anyway, hopefully, this has been a helpful overview of how Zenpy works. If you have any questions, just ask!
103 changes: 78 additions & 25 deletions docs/api_objects.rst
@@ -1,12 +1,18 @@
API Objects
===========

The Zenpy API objects are automatically generated using the script `gen_classes.py` in the `tools` directory based on the specification found in the `specification` directory. The generated Python objects are a one to one mapping with the API, so any attribute returned by the API should be present in the Python object.
The Zenpy API objects are automatically generated using the script
`gen_classes.py` in the `tools` directory based on the specification found in
the `specification` directory. The generated Python objects are a one to one
mapping with the API, so any attribute returned by the API should be present
in the Python object.

Instantiating Objects
---------------------

When creating objects, any keyword argument can be passed to the constructor and it will become an attribute of that object. For example the code:
When creating objects, any keyword argument can be passed to the constructor
and it will become an attribute of that object. For example the code:

::

user = User(name='Jim', email='jim@jim.com')
Expand All @@ -16,7 +22,14 @@ Will create a `User` object with the name and email fields set.
Object Properties
-----------------

Many attributes are implemented as `properties`. Any attribute that holds an id is also implemented as a `property` which returns the object associated with that id. For example, the `Ticket` object has two related attributes: `assignee` and `assignee_id`. When the `assignee` attribute is accessed, Zenpy first attempts to locate the related `User` in the `User` cache and if it cannot be found will generate and execute an API call to retrieve, instantiate, cache and return the object. Accessing the `assignee_id` attribute simply returns the id.
Many attributes are implemented as `properties`. Any attribute that holds an
id is also implemented as a `property` which returns the object associated
with that id. For example, the `Ticket` object has two related attributes:
`assignee` and `assignee_id`. When the `assignee` attribute is accessed,
Zenpy first attempts to locate the related `User` in the `User` cache and if
it cannot be found will generate and execute an API call to retrieve,
instantiate, cache and return the object. Accessing the `assignee_id`
attribute simply returns the id.

The following attributes are also implemented as `properties`:

Expand All @@ -31,7 +44,11 @@ The following attributes are also implemented as `properties`:
+--------------------+----------------------------+


It is important to note that most property setters throw away all information except for the id. This is because Zendesk only expects the id, so any modifications made to the object will not be persisted automatically. For example, the following `Ticket`::
It is important to note that most property setters throw away all information
except for the id. This is because Zendesk only expects the id, so any
modifications made to the object will not be persisted automatically.

For example, the following `Ticket`::

ticket = Ticket(subject='stuffs', description='stuff stuff stuff')
user = User(id=10, name='Jim', email='jim@jim.com')
Expand All @@ -49,7 +66,13 @@ Will be serialized as::
Object Serialization
--------------------

Before API objects are sent to Zendesk (eg for creation/updates), they are serialized into JSON once again. This is done recursively and any nested objects will be serialized as well. One handy side effect of how this is done is it is possible to set any arbitrary attribute on an API object and it will be serialized and sent to Zendesk. For example the object::
Before API objects are sent to Zendesk (eg for creation/updates), they are
serialized into JSON once again. This is done recursively and any nested
objects will be serialized as well. One handy side effect of how this is done
is it is possible to set any arbitrary attribute on an API object and it will
be serialized and sent to Zendesk.

For example the object::

user = User(name='Jim', email='jim@jim.com')
user.some_thing = 100
Expand All @@ -63,52 +86,62 @@ Will be serialized as::
}


A more useful example might be adding a comment to a ticket. Usually the `Ticket` object does not have a `comment` attribute, however if we add one it will be correctly serialized, sent to Zendesk and added to the specified ticket.
For example, after adding the `comment` attribute containing a `Comment` object::
A more useful example might be adding a comment to a ticket. Usually the
`Ticket` object does not have a `comment` attribute, however if we add one it
will be correctly serialized, sent to Zendesk and added to the specified
ticket.

For example, after adding the `comment` attribute containing a `Comment`
object::

ticket.comment = Comment(body="I am a private comment", public=False)

the serialized ticket will include the additional information::

{
"description": "I am a test ticket",
"subject": "Testing",

...snip...
"comment": {
"public": false,
"body": "I am a private comment"
}
}
{
"description": "I am a test ticket",
"subject": "Testing",

...snip...
"comment": {
"public": false,
"body": "I am a private comment"
}
}


Api Object Modifications
------------------------

When updating an object, only the id and those attributes that have been modified will be sent to Zendesk. For example, the following code will only print the user's id.
When updating an object, only the id and those attributes that have been
modified will be sent to Zendesk. For example, the following code will only
print the user's id.

.. code:: python
user = zenpy_client.users.me()
print(user.to_dict(serialize=True))
Whereas the following will also include the name attribute, as it has been modified:
Whereas the following will also include the name attribute, as it has been
modified:

.. code:: python
user = zenpy_client.users.me()
user.name = "Batman"
print(user.to_dict(serialize=True))
This is also true of attributes of objects that are lists or dicts, however the implementation might lead to some surprises. Consider the following:
This is also true of attributes of objects that are lists or dicts, however
the implementation might lead to some surprises. Consider the following:

.. code:: python
ticket = zenpy_client.tickets(id=1)
ticket.custom_fields[0]['value'] = 'I am modified'
print(ticket.to_dict(serialize=True))
Here we modify the first element of the custom_fields list. How can Zenpy know that this has happened? The answer is proxy objects:
Here we modify the first element of the custom_fields list. How can Zenpy know
that this has happened? The answer is proxy objects:

.. code:: python
Expand All @@ -118,12 +151,32 @@ Here we modify the first element of the custom_fields list. How can Zenpy know t
print(type(ticket.custom_fields[0]))
>> <class 'zenpy.lib.proxy.ProxyDict'>
The way this works is when an element is retrieved from a list, it checks whether or not it is a list or dict. If it is, then the list or dict is wrapped in a Proxy class which executes a callback on modification so that the parent knows it has been modified. As a result, we can detect changes to lists or dicts and properly update Zendesk when they occur.
The way this works is when an element is retrieved from a list, it checks
whether or not it is a list or dict. If it is, then the list or dict is
wrapped in a Proxy class which executes a callback on modification so that the
parent knows it has been modified. As a result, we can detect changes to lists
or dicts and properly update Zendesk when they occur.


Api Object Reference
--------------------
Api Objects Reference
---------------------

.. automodule:: zenpy.lib.api_objects
:members:
:undoc-members:
:undoc-members:

Cache Objects Reference
-----------------------

.. automodule:: zenpy.lib.cache
:members:
:undoc-members:

Exceptions Objects Reference
----------------------------

Here is complete list of all exceptions implemented in :class:`Zenpy`.

.. automodule:: zenpy.lib.exception
:members:
:undoc-members:
5 changes: 3 additions & 2 deletions docs/conf.py
Expand Up @@ -14,6 +14,7 @@

import sys
import os
from datetime import datetime

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
Expand Down Expand Up @@ -59,7 +60,7 @@

# General information about the project.
project = u'Zenpy'
copyright = u'2015, Facetoe'
copyright = u'2015-{}, Facetoe and community authors'.format(datetime.today().year)
author = u'Facetoe'

# The version info for the project you're documenting, acts as replacement for
Expand Down Expand Up @@ -151,7 +152,7 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# html_static_path = ['_static']

# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
Expand Down
26 changes: 19 additions & 7 deletions docs/zenpy.rst
Expand Up @@ -130,20 +130,32 @@ Querying the API
----------------

The :class:`Zenpy` object contains methods for accessing many top level
endpoints, and they can be called in one of two ways - no arguments
endpoints, and they can be called in one of three ways - no arguments
returns all results (as a generator):

.. code:: python
for user in zenpy_client.users():
print user.name
And called with an ID returns the object with that ID:
Called with an `id` returns the object with that ID:

.. code:: python
print zenpy_client.users(id=1159307768)
And the last option for many endpoints to get list of several items, use
`ids` for this. Accepts lists of ids, not list of objects! ``show_many.json``
should exist in Zendesk endpoint, search API
`docs <https://developer.zendesk.com/rest_api/docs/zendesk-apis/resources>`__.

Example with several ids, returns generator objects:

.. code:: python
print zenpy_client.users(ids=[1000000001, 1000000002])
You can also filter by passing in ``permission_set`` or ``role``.

In addition to the top level endpoints there are several secondary level
Expand Down Expand Up @@ -248,16 +260,16 @@ the number of objects that can be processed at one time (usually 100).
``APIException`` if that limit is exceeded, however some simply process
the first N objects and silently discard the rest.

2. On high intensive job loads (intensive imports, permanent delete operations, etc)
Zendesk side API does not return `/api/v2/job_statuses/{job_id}.json` page, so if you
try to query it with:
2. On high intensive job loads (intensive imports, permanent delete operations,
etc) Zendesk side API does not return `/api/v2/job_statuses/{job_id}.json`
page, so if you try to query it with:

.. code:: python
job_status = zenpy_client.job_status(id={job_id})
you will get ``HTTPError``. In same time page: `/api/v2/job_statuses/` always exist and
contains last 100 jobs. So parse whole job list to get results:
you will get ``HTTPError``. In same time page: `/api/v2/job_statuses/` always
exist and contains last 100 jobs. So parse whole job list to get results:

.. code:: python
Expand Down
22 changes: 11 additions & 11 deletions requirements.dev
@@ -1,15 +1,15 @@
sphinx>1.5
pycodestyle>=1.5.7
betamax>=0.8.0
cachetools>=3.1.0
requests>=2.14.2
pytz>=2018.9
future>=0.17.1
python-dateutil>=2.7.5
sphinx>=1.8.4
pycodestyle>=2.5.0
betamax>=0.8.1
betamax-matchers>=0.4.0
betamax-serializers>=0.2.0
python-dateutil>=2.6.0
cachetools>=2.0.0
nose>=1.3.7
requests==2.14.2
yapf>=0.18.0
yapf>=0.26.0
bs4>=0.0.1
lxml>=4.0.0
pytz>=2017.3
future>=0.16.0
sphinx_rtd_theme
lxml>=4.3.1
sphinx_rtd_theme>=0.4.3
8 changes: 4 additions & 4 deletions requirements.txt
@@ -1,5 +1,5 @@
cachetools>=2.0.0
cachetools>=3.1.0
requests>=2.14.2
pytz>=2017.3
future>=0.16.0
python-dateutil>=2.6.0
pytz>=2018.9
future>=0.17.1
python-dateutil>=2.7.5

0 comments on commit c7e84ce

Please sign in to comment.