zammad_py 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zammad_py-3.2.0/LICENSE +21 -0
- zammad_py-3.2.0/PKG-INFO +138 -0
- zammad_py-3.2.0/README.rst +114 -0
- zammad_py-3.2.0/pyproject.toml +37 -0
- zammad_py-3.2.0/zammad_py/__init__.py +7 -0
- zammad_py-3.2.0/zammad_py/api.py +573 -0
- zammad_py-3.2.0/zammad_py/exceptions.py +2 -0
zammad_py-3.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Joe Paul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
zammad_py-3.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: zammad_py
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: Python API client for zammad
|
|
5
|
+
Home-page: https://github.com/joeirimpan/zammad_py
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Joe Paul
|
|
8
|
+
Author-email: joeirimpan@gmail.com
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Dist: requests (>=2.25.1,<3.0.0)
|
|
20
|
+
Project-URL: Documentation, https://zammad-py.readthedocs.io/
|
|
21
|
+
Project-URL: Repository, https://github.com/joeirimpan/zammad_py.git
|
|
22
|
+
Description-Content-Type: text/x-rst
|
|
23
|
+
|
|
24
|
+
=================
|
|
25
|
+
Zammad API Client
|
|
26
|
+
=================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
.. image:: https://img.shields.io/pypi/v/zammad_py.svg
|
|
30
|
+
:target: https://pypi.python.org/pypi/zammad_py
|
|
31
|
+
|
|
32
|
+
.. image:: https://img.shields.io/travis/joeirimpan/zammad_py.svg
|
|
33
|
+
:target: https://travis-ci.org/joeirimpan/zammad_py
|
|
34
|
+
|
|
35
|
+
.. image:: https://readthedocs.org/projects/zammad-py/badge/?version=latest
|
|
36
|
+
:target: https://zammad-py.readthedocs.io/en/latest/?badge=latest
|
|
37
|
+
:alt: Documentation Status
|
|
38
|
+
|
|
39
|
+
.. image:: https://pyup.io/repos/github/joeirimpan/zammad_py/shield.svg
|
|
40
|
+
:target: https://pyup.io/repos/github/joeirimpan/zammad_py/
|
|
41
|
+
:alt: Updates
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Python API client for zammad
|
|
45
|
+
|
|
46
|
+
* Free software: MIT license
|
|
47
|
+
* Documentation: https://zammad-py.readthedocs.io.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Quickstart
|
|
51
|
+
----------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
.. code-block:: python
|
|
55
|
+
|
|
56
|
+
from zammad_py import ZammadAPI
|
|
57
|
+
|
|
58
|
+
# Initialize the client with the URL, username, and password
|
|
59
|
+
# Note the Host URL should be in this format: 'https://zammad.example.org/api/v1/'
|
|
60
|
+
client = ZammadAPI(url='<HOST>', username='<USERNAME>', password='<PASSWORD>')
|
|
61
|
+
|
|
62
|
+
# Example: Access all users
|
|
63
|
+
this_page = client.user.all()
|
|
64
|
+
for user in this_page:
|
|
65
|
+
print(user)
|
|
66
|
+
|
|
67
|
+
# Example: Get information about the current user
|
|
68
|
+
print(client.user.me())
|
|
69
|
+
|
|
70
|
+
# Example: Create a ticket
|
|
71
|
+
params = {
|
|
72
|
+
"title": "Help me!",
|
|
73
|
+
"group": "2nd Level",
|
|
74
|
+
"customer": "david@example.com",
|
|
75
|
+
"article": {
|
|
76
|
+
"subject": "My subject",
|
|
77
|
+
"body": "I am a message!",
|
|
78
|
+
"type": "note",
|
|
79
|
+
"internal": false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
new_ticket = client.ticket.create(params=params)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
General Methods
|
|
87
|
+
---------------
|
|
88
|
+
Most resources support these methods:
|
|
89
|
+
|
|
90
|
+
`.all()`: Returns a paginated response with the current page number and a list of elements.
|
|
91
|
+
|
|
92
|
+
`.next_page()`: Returns the next page of the current pagination object.
|
|
93
|
+
|
|
94
|
+
`.prev_page()`: Returns the previous page of the current pagination object.
|
|
95
|
+
|
|
96
|
+
`.search(params)`: Returns a paginated response based on the search parameters.
|
|
97
|
+
|
|
98
|
+
`.find(id)`: Returns a single object with the specified ID.
|
|
99
|
+
|
|
100
|
+
`.create(params)`: Creates a new object with the specified parameters.
|
|
101
|
+
|
|
102
|
+
`.update(params)`: Updates an existing object with the specified parameters.
|
|
103
|
+
|
|
104
|
+
`.destroy(id)`: Deletes an object with the specified ID.
|
|
105
|
+
|
|
106
|
+
Additional Resource Methods
|
|
107
|
+
---------------------------
|
|
108
|
+
User resource also has the .me() method to get information about the current user.
|
|
109
|
+
|
|
110
|
+
Ticket resource also has the .articles() method to get the articles associated with a ticket.
|
|
111
|
+
|
|
112
|
+
Link resource has methods to list, add, and delete links between objects.
|
|
113
|
+
|
|
114
|
+
TicketArticleAttachment resource has the .download() method to download a ticket attachment.
|
|
115
|
+
|
|
116
|
+
Object resource has the .execute_migrations() method to run migrations on an object.
|
|
117
|
+
|
|
118
|
+
You can set the `on_behalf_of` attribute of the ZammadAPI instance to do actions on behalf of another user.
|
|
119
|
+
|
|
120
|
+
Contributing
|
|
121
|
+
------------
|
|
122
|
+
The Zammad API Client (zammad_py) welcomes contributions.
|
|
123
|
+
|
|
124
|
+
You can contribute by reporting bugs, fixing bugs, implementing new features, writing documentation, and submitting feedback.
|
|
125
|
+
|
|
126
|
+
To get started, see the contributing section in the docs!
|
|
127
|
+
|
|
128
|
+
Please ensure that your changes include tests and updated documentation if necessary.
|
|
129
|
+
|
|
130
|
+
Credits
|
|
131
|
+
-------
|
|
132
|
+
|
|
133
|
+
This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
|
|
134
|
+
|
|
135
|
+
.. _Cookiecutter: https://github.com/audreyr/cookiecutter
|
|
136
|
+
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
|
|
137
|
+
|
|
138
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
=================
|
|
2
|
+
Zammad API Client
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
.. image:: https://img.shields.io/pypi/v/zammad_py.svg
|
|
7
|
+
:target: https://pypi.python.org/pypi/zammad_py
|
|
8
|
+
|
|
9
|
+
.. image:: https://img.shields.io/travis/joeirimpan/zammad_py.svg
|
|
10
|
+
:target: https://travis-ci.org/joeirimpan/zammad_py
|
|
11
|
+
|
|
12
|
+
.. image:: https://readthedocs.org/projects/zammad-py/badge/?version=latest
|
|
13
|
+
:target: https://zammad-py.readthedocs.io/en/latest/?badge=latest
|
|
14
|
+
:alt: Documentation Status
|
|
15
|
+
|
|
16
|
+
.. image:: https://pyup.io/repos/github/joeirimpan/zammad_py/shield.svg
|
|
17
|
+
:target: https://pyup.io/repos/github/joeirimpan/zammad_py/
|
|
18
|
+
:alt: Updates
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Python API client for zammad
|
|
22
|
+
|
|
23
|
+
* Free software: MIT license
|
|
24
|
+
* Documentation: https://zammad-py.readthedocs.io.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Quickstart
|
|
28
|
+
----------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
.. code-block:: python
|
|
32
|
+
|
|
33
|
+
from zammad_py import ZammadAPI
|
|
34
|
+
|
|
35
|
+
# Initialize the client with the URL, username, and password
|
|
36
|
+
# Note the Host URL should be in this format: 'https://zammad.example.org/api/v1/'
|
|
37
|
+
client = ZammadAPI(url='<HOST>', username='<USERNAME>', password='<PASSWORD>')
|
|
38
|
+
|
|
39
|
+
# Example: Access all users
|
|
40
|
+
this_page = client.user.all()
|
|
41
|
+
for user in this_page:
|
|
42
|
+
print(user)
|
|
43
|
+
|
|
44
|
+
# Example: Get information about the current user
|
|
45
|
+
print(client.user.me())
|
|
46
|
+
|
|
47
|
+
# Example: Create a ticket
|
|
48
|
+
params = {
|
|
49
|
+
"title": "Help me!",
|
|
50
|
+
"group": "2nd Level",
|
|
51
|
+
"customer": "david@example.com",
|
|
52
|
+
"article": {
|
|
53
|
+
"subject": "My subject",
|
|
54
|
+
"body": "I am a message!",
|
|
55
|
+
"type": "note",
|
|
56
|
+
"internal": false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
new_ticket = client.ticket.create(params=params)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
General Methods
|
|
64
|
+
---------------
|
|
65
|
+
Most resources support these methods:
|
|
66
|
+
|
|
67
|
+
`.all()`: Returns a paginated response with the current page number and a list of elements.
|
|
68
|
+
|
|
69
|
+
`.next_page()`: Returns the next page of the current pagination object.
|
|
70
|
+
|
|
71
|
+
`.prev_page()`: Returns the previous page of the current pagination object.
|
|
72
|
+
|
|
73
|
+
`.search(params)`: Returns a paginated response based on the search parameters.
|
|
74
|
+
|
|
75
|
+
`.find(id)`: Returns a single object with the specified ID.
|
|
76
|
+
|
|
77
|
+
`.create(params)`: Creates a new object with the specified parameters.
|
|
78
|
+
|
|
79
|
+
`.update(params)`: Updates an existing object with the specified parameters.
|
|
80
|
+
|
|
81
|
+
`.destroy(id)`: Deletes an object with the specified ID.
|
|
82
|
+
|
|
83
|
+
Additional Resource Methods
|
|
84
|
+
---------------------------
|
|
85
|
+
User resource also has the .me() method to get information about the current user.
|
|
86
|
+
|
|
87
|
+
Ticket resource also has the .articles() method to get the articles associated with a ticket.
|
|
88
|
+
|
|
89
|
+
Link resource has methods to list, add, and delete links between objects.
|
|
90
|
+
|
|
91
|
+
TicketArticleAttachment resource has the .download() method to download a ticket attachment.
|
|
92
|
+
|
|
93
|
+
Object resource has the .execute_migrations() method to run migrations on an object.
|
|
94
|
+
|
|
95
|
+
You can set the `on_behalf_of` attribute of the ZammadAPI instance to do actions on behalf of another user.
|
|
96
|
+
|
|
97
|
+
Contributing
|
|
98
|
+
------------
|
|
99
|
+
The Zammad API Client (zammad_py) welcomes contributions.
|
|
100
|
+
|
|
101
|
+
You can contribute by reporting bugs, fixing bugs, implementing new features, writing documentation, and submitting feedback.
|
|
102
|
+
|
|
103
|
+
To get started, see the contributing section in the docs!
|
|
104
|
+
|
|
105
|
+
Please ensure that your changes include tests and updated documentation if necessary.
|
|
106
|
+
|
|
107
|
+
Credits
|
|
108
|
+
-------
|
|
109
|
+
|
|
110
|
+
This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
|
|
111
|
+
|
|
112
|
+
.. _Cookiecutter: https://github.com/audreyr/cookiecutter
|
|
113
|
+
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
|
|
114
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "zammad_py"
|
|
3
|
+
version = "3.2.0"
|
|
4
|
+
readme = "README.rst"
|
|
5
|
+
description = "Python API client for zammad"
|
|
6
|
+
authors = ["Joe Paul <joeirimpan@gmail.com>"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
classifiers = [
|
|
9
|
+
'Development Status :: 4 - Beta',
|
|
10
|
+
'Intended Audience :: Developers',
|
|
11
|
+
'License :: OSI Approved :: MIT License',
|
|
12
|
+
'Natural Language :: English',
|
|
13
|
+
'Programming Language :: Python :: 3.9',
|
|
14
|
+
'Programming Language :: Python :: 3.10',
|
|
15
|
+
]
|
|
16
|
+
homepage = "https://github.com/joeirimpan/zammad_py"
|
|
17
|
+
documentation = "https://zammad-py.readthedocs.io/"
|
|
18
|
+
repository = "https://github.com/joeirimpan/zammad_py.git"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.dependencies]
|
|
21
|
+
python = ">=3.9,<4.0"
|
|
22
|
+
requests = "^2.25.1"
|
|
23
|
+
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
25
|
+
pytest = "^8.0.0"
|
|
26
|
+
vcrpy = "^5.1.0"
|
|
27
|
+
flake8 = "7.2.0"
|
|
28
|
+
black = "24.1.1"
|
|
29
|
+
isort = "5.13.2"
|
|
30
|
+
pytest-vcr = "^1.0.2"
|
|
31
|
+
|
|
32
|
+
[tool.mypy]
|
|
33
|
+
files = "zammad_py"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["poetry-core>=1.0.0"]
|
|
37
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"""Main module."""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any, Generator, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from requests.exceptions import HTTPError
|
|
10
|
+
|
|
11
|
+
from zammad_py.exceptions import ConfigException
|
|
12
|
+
|
|
13
|
+
__all__ = ["ZammadAPI"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ZammadAPI:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
url: str,
|
|
20
|
+
username: Optional[str] = None,
|
|
21
|
+
password: Optional[str] = None,
|
|
22
|
+
http_token: Optional[str] = None,
|
|
23
|
+
oauth2_token: Optional[str] = None,
|
|
24
|
+
on_behalf_of: Optional[str] = None,
|
|
25
|
+
additional_headers: Optional[List[Tuple[str, str]]] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.url = url if url.endswith("/") else f"{url}/"
|
|
28
|
+
self._username = username
|
|
29
|
+
self._password = password
|
|
30
|
+
self._http_token = http_token
|
|
31
|
+
self._oauth2_token = oauth2_token
|
|
32
|
+
self._on_behalf_of = on_behalf_of
|
|
33
|
+
self._additional_headers = additional_headers
|
|
34
|
+
self._check_config()
|
|
35
|
+
|
|
36
|
+
self.session = requests.Session()
|
|
37
|
+
atexit.register(self.session.close)
|
|
38
|
+
self.session.headers["User-Agent"] = "Zammad API Python"
|
|
39
|
+
if self._http_token:
|
|
40
|
+
self.session.headers["Authorization"] = "Token token=%s" % self._http_token
|
|
41
|
+
elif oauth2_token:
|
|
42
|
+
self.session.headers["Authorization"] = "Bearer %s" % self._oauth2_token
|
|
43
|
+
elif self._username and self._password: # noqa: SIM106
|
|
44
|
+
self.session.auth = (self._username, self._password)
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("Invalid Authentication information in config")
|
|
47
|
+
|
|
48
|
+
if self._on_behalf_of:
|
|
49
|
+
self.session.headers["X-On-Behalf-Of"] = self._on_behalf_of
|
|
50
|
+
|
|
51
|
+
if self._additional_headers:
|
|
52
|
+
for additional_header in self._additional_headers:
|
|
53
|
+
self.session.headers[additional_header[0]] = additional_header[1]
|
|
54
|
+
|
|
55
|
+
def _check_config(self) -> None:
|
|
56
|
+
"""Check the configuration"""
|
|
57
|
+
if not self.url:
|
|
58
|
+
raise ConfigException("Missing url in config")
|
|
59
|
+
if self._http_token:
|
|
60
|
+
return
|
|
61
|
+
if self._oauth2_token:
|
|
62
|
+
return
|
|
63
|
+
if not self._username:
|
|
64
|
+
raise ConfigException("Missing username in config")
|
|
65
|
+
if not self._password:
|
|
66
|
+
raise ConfigException("Missing password in config")
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def on_behalf_of(self) -> Optional[str]:
|
|
70
|
+
return self._on_behalf_of
|
|
71
|
+
|
|
72
|
+
@on_behalf_of.setter
|
|
73
|
+
def on_behalf_of(self, value: str) -> None:
|
|
74
|
+
self._on_behalf_of = value
|
|
75
|
+
self.session.headers["X-On-Behalf-Of"] = self._on_behalf_of
|
|
76
|
+
|
|
77
|
+
@contextmanager
|
|
78
|
+
def request_on_behalf_of(
|
|
79
|
+
self, on_behalf_of: str
|
|
80
|
+
) -> Generator["ZammadAPI", None, None]:
|
|
81
|
+
"""
|
|
82
|
+
Use X-On-Behalf-Of Header, see https://docs.zammad.org/en/latest/api/intro.html?highlight=on%20behalf#actions-on-behalf-of-other-users
|
|
83
|
+
|
|
84
|
+
:param on_behalf_of: The value of this header can be one of the following: user ID, login or email
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
initial_value = self.session.headers["X-On-Behalf-Of"]
|
|
88
|
+
self.session.headers["X-On-Behalf-Of"] = on_behalf_of
|
|
89
|
+
yield self
|
|
90
|
+
self.session.headers["X-On-Behalf-Of"] = initial_value
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def group(self) -> "Group":
|
|
94
|
+
"""Return a `Group` instance"""
|
|
95
|
+
return Group(connection=self)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def organization(self) -> "Organization":
|
|
99
|
+
"""Return a `Organization` instance"""
|
|
100
|
+
return Organization(connection=self)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def role(self) -> "Role":
|
|
104
|
+
"""Return a `Role` instance"""
|
|
105
|
+
return Role(connection=self)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def ticket(self) -> "Ticket":
|
|
109
|
+
"""Return a `Ticket` instance"""
|
|
110
|
+
return Ticket(connection=self)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def link(self):
|
|
114
|
+
"""Return a `Link` instance"""
|
|
115
|
+
return Link(connection=self)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def ticket_article(self) -> "TicketArticle":
|
|
119
|
+
"""Return a `TicketArticle` instance"""
|
|
120
|
+
return TicketArticle(connection=self)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def ticket_article_attachment(self) -> "TicketArticleAttachment":
|
|
124
|
+
"""Return a `TicketArticleAttachment` instance"""
|
|
125
|
+
return TicketArticleAttachment(connection=self)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def ticket_article_plain(self) -> "TicketArticlePlain":
|
|
129
|
+
"""Return a `TicketArticlePlain` instance"""
|
|
130
|
+
return TicketArticlePlain(connection=self)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def ticket_priority(self) -> "TicketPriority":
|
|
134
|
+
"""Return a `TicketPriority` instance"""
|
|
135
|
+
return TicketPriority(connection=self)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def ticket_state(self) -> "TicketState":
|
|
139
|
+
"""Return a `TicketState` instance"""
|
|
140
|
+
return TicketState(connection=self)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def user(self) -> "User":
|
|
144
|
+
"""Return a `User` instance"""
|
|
145
|
+
return User(connection=self)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def taglist(self) -> "TagList":
|
|
149
|
+
"""Retrun a TagList instance"""
|
|
150
|
+
return TagList(connection=self)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def ticket_tag(self):
|
|
154
|
+
"""Return a `TicketTag` instance"""
|
|
155
|
+
return TicketTag(connection=self)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Pagination:
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
items,
|
|
162
|
+
resource: "Resource",
|
|
163
|
+
function_name: str,
|
|
164
|
+
params=None,
|
|
165
|
+
page: int = 1,
|
|
166
|
+
) -> None:
|
|
167
|
+
self._items = items
|
|
168
|
+
self._page = page
|
|
169
|
+
self._resource = resource
|
|
170
|
+
# Create a copy of params and remove page to prevent it from overriding the incremented page value
|
|
171
|
+
self._params = params.copy() if params else {}
|
|
172
|
+
if (
|
|
173
|
+
self._params
|
|
174
|
+
and "filters" in self._params
|
|
175
|
+
and isinstance(self._params["filters"], dict)
|
|
176
|
+
and "page" in self._params["filters"]
|
|
177
|
+
):
|
|
178
|
+
self._params["filters"] = self._params["filters"].copy()
|
|
179
|
+
self._params["filters"].pop("page", None)
|
|
180
|
+
self._function_name = function_name
|
|
181
|
+
|
|
182
|
+
def is_last_page(self) -> bool:
|
|
183
|
+
"""Check if the current page is the last page"""
|
|
184
|
+
if len(self._items) < self._resource.per_page:
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def __iter__(self):
|
|
189
|
+
yield from self._items
|
|
190
|
+
|
|
191
|
+
def __len__(self) -> int:
|
|
192
|
+
return len(self._items)
|
|
193
|
+
|
|
194
|
+
def __getitem__(self, index: int):
|
|
195
|
+
return self._items[index]
|
|
196
|
+
|
|
197
|
+
def __setitem__(self, index: int, value) -> None:
|
|
198
|
+
self._items[index] = value
|
|
199
|
+
|
|
200
|
+
def next_page(self) -> "Pagination":
|
|
201
|
+
self._page += 1
|
|
202
|
+
return getattr(self._resource, self._function_name)(
|
|
203
|
+
page=self._page, **self._params
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def prev_page(self) -> "Pagination":
|
|
207
|
+
self._page -= 1
|
|
208
|
+
return getattr(self._resource, self._function_name)(
|
|
209
|
+
page=self._page, **self._params
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Resource(ABC):
|
|
214
|
+
def __init__(self, connection: ZammadAPI, per_page: int = 10) -> None:
|
|
215
|
+
self._connection = connection
|
|
216
|
+
self._per_page = per_page
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
@abstractmethod
|
|
220
|
+
def path_attribute(self) -> str:
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def url(self) -> str:
|
|
225
|
+
"""Returns a the full url concatenated with the resource class name"""
|
|
226
|
+
return self._connection.url + self.path_attribute
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def per_page(self) -> int:
|
|
230
|
+
return self._per_page
|
|
231
|
+
|
|
232
|
+
@per_page.setter
|
|
233
|
+
def per_page(self, value: int) -> None:
|
|
234
|
+
self._per_page = value
|
|
235
|
+
|
|
236
|
+
def _raise_or_return_json(self, response: requests.Response) -> Any:
|
|
237
|
+
"""Raise HTTPError before converting response to json
|
|
238
|
+
|
|
239
|
+
:param response: Request response object
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
response.raise_for_status()
|
|
243
|
+
except HTTPError:
|
|
244
|
+
raise HTTPError(response.text)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
json_value = response.json()
|
|
248
|
+
except ValueError:
|
|
249
|
+
return response.content
|
|
250
|
+
else:
|
|
251
|
+
return json_value
|
|
252
|
+
|
|
253
|
+
def all(self, page: int = 1, filters=None) -> Pagination:
|
|
254
|
+
"""Returns the list of resources
|
|
255
|
+
|
|
256
|
+
:param page: Page number
|
|
257
|
+
:param filters: Filter arguments including page, per_page if needed
|
|
258
|
+
"""
|
|
259
|
+
params = filters.copy() if filters else {}
|
|
260
|
+
|
|
261
|
+
# Set defaults only if not specified in filters
|
|
262
|
+
if "page" not in params:
|
|
263
|
+
params["page"] = page
|
|
264
|
+
if "per_page" not in params:
|
|
265
|
+
params["per_page"] = self._per_page
|
|
266
|
+
if "expand" not in params:
|
|
267
|
+
params["expand"] = "true"
|
|
268
|
+
|
|
269
|
+
if "per_page" in params:
|
|
270
|
+
self._per_page = params["per_page"]
|
|
271
|
+
|
|
272
|
+
response = self._connection.session.get(self.url, params=params)
|
|
273
|
+
data = self._raise_or_return_json(response)
|
|
274
|
+
|
|
275
|
+
return Pagination(
|
|
276
|
+
items=data,
|
|
277
|
+
resource=self,
|
|
278
|
+
function_name="all",
|
|
279
|
+
params={"filters": params},
|
|
280
|
+
page=params["page"],
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def search(self, search_string: str, page: int = 1, filters=None) -> Pagination:
|
|
284
|
+
"""Returns the list of resources
|
|
285
|
+
|
|
286
|
+
:param search_string: option to filter for
|
|
287
|
+
:param page: Page number
|
|
288
|
+
:param filters: Filter arguments like page, per_page
|
|
289
|
+
"""
|
|
290
|
+
params = filters.copy() if filters else {}
|
|
291
|
+
params.update({"query": search_string})
|
|
292
|
+
|
|
293
|
+
# Set defaults only if not specified in filters
|
|
294
|
+
if "page" not in params:
|
|
295
|
+
params["page"] = page
|
|
296
|
+
if "per_page" not in params:
|
|
297
|
+
params["per_page"] = self._per_page
|
|
298
|
+
if "expand" not in params:
|
|
299
|
+
params["expand"] = "true"
|
|
300
|
+
|
|
301
|
+
if "per_page" in params:
|
|
302
|
+
self._per_page = params["per_page"]
|
|
303
|
+
|
|
304
|
+
response = self._connection.session.get(self.url + "/search", params=params)
|
|
305
|
+
data = self._raise_or_return_json(response)
|
|
306
|
+
|
|
307
|
+
return Pagination(
|
|
308
|
+
items=data,
|
|
309
|
+
resource=self,
|
|
310
|
+
function_name="search",
|
|
311
|
+
params={"search_string": search_string, "filters": params},
|
|
312
|
+
page=params["page"],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def find(self, id):
|
|
316
|
+
"""Return the resource associated with the id
|
|
317
|
+
|
|
318
|
+
:param id: Resource id
|
|
319
|
+
"""
|
|
320
|
+
response = self._connection.session.get(self.url + "/%s" % id)
|
|
321
|
+
return self._raise_or_return_json(response)
|
|
322
|
+
|
|
323
|
+
def create(self, params):
|
|
324
|
+
"""Create the requested resource
|
|
325
|
+
|
|
326
|
+
:param params: Resource data for creating
|
|
327
|
+
"""
|
|
328
|
+
response = self._connection.session.post(self.url, json=params)
|
|
329
|
+
return self._raise_or_return_json(response)
|
|
330
|
+
|
|
331
|
+
def update(self, id, params):
|
|
332
|
+
"""Update the requested resource
|
|
333
|
+
|
|
334
|
+
:param id: Resource id
|
|
335
|
+
:param params: Resource data for updating
|
|
336
|
+
"""
|
|
337
|
+
response = self._connection.session.put(self.url + "/%s" % id, json=params)
|
|
338
|
+
return self._raise_or_return_json(response)
|
|
339
|
+
|
|
340
|
+
def destroy(self, id):
|
|
341
|
+
"""Delete the resource associated with the id
|
|
342
|
+
|
|
343
|
+
:param id: Resource id
|
|
344
|
+
"""
|
|
345
|
+
response = self._connection.session.delete(self.url + "/%s" % id)
|
|
346
|
+
return self._raise_or_return_json(response)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class Group(Resource):
|
|
350
|
+
path_attribute = "groups"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Role(Resource):
|
|
354
|
+
path_attribute = "roles"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class Organization(Resource):
|
|
358
|
+
path_attribute = "organizations"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class Ticket(Resource):
|
|
362
|
+
path_attribute = "tickets"
|
|
363
|
+
|
|
364
|
+
def articles(self, id):
|
|
365
|
+
"""Returns all the articles associated with the ticket id
|
|
366
|
+
|
|
367
|
+
:param id: Ticket id
|
|
368
|
+
"""
|
|
369
|
+
response = self._connection.session.get(
|
|
370
|
+
self._connection.url + "ticket_articles/by_ticket/%s?expand=true" % id
|
|
371
|
+
)
|
|
372
|
+
return self._raise_or_return_json(response)
|
|
373
|
+
|
|
374
|
+
def tags(self, id):
|
|
375
|
+
"""Returns all the tags associated with the ticket id
|
|
376
|
+
|
|
377
|
+
:param id: Ticket id
|
|
378
|
+
"""
|
|
379
|
+
response = self._connection.session.get(
|
|
380
|
+
self._connection.url + f"tags?object=Ticket&o_id={id}"
|
|
381
|
+
)
|
|
382
|
+
return self._raise_or_return_json(response)
|
|
383
|
+
|
|
384
|
+
def merge(self, id, number):
|
|
385
|
+
"""Merges two tickets, (undocumented in Zammad Docs)
|
|
386
|
+
If the objects are already merged, it will return "Object already exists!"
|
|
387
|
+
Attention: Must use password to authenticate to Zammad, otherwise this will not work!
|
|
388
|
+
:param id: Ticket id of the child
|
|
389
|
+
:param number: Ticket Number of the Parent
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
response = self._connection.session.put(
|
|
393
|
+
self._connection.url + f"ticket_merge/{id}/{number}"
|
|
394
|
+
)
|
|
395
|
+
return self._raise_or_return_json(response)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class Link(Resource):
|
|
399
|
+
path_attribute = "links"
|
|
400
|
+
|
|
401
|
+
def add(
|
|
402
|
+
self,
|
|
403
|
+
link_object_target_value,
|
|
404
|
+
link_object_source_number,
|
|
405
|
+
link_type="normal",
|
|
406
|
+
link_object_target="Ticket",
|
|
407
|
+
link_object_source="Ticket",
|
|
408
|
+
):
|
|
409
|
+
"""Create the link
|
|
410
|
+
|
|
411
|
+
:params link_type: Link type ('normal', 'parent', 'child')
|
|
412
|
+
:params link_object_target: (for now*: 'Ticket')
|
|
413
|
+
:params link_object_target_value: Ticket ID
|
|
414
|
+
:params link_object_source: (for now*: 'Ticket')
|
|
415
|
+
:params link_object_source_number: Ticket Number (Not the ID!)
|
|
416
|
+
|
|
417
|
+
*Currently, only Tickets can be linked together.
|
|
418
|
+
"""
|
|
419
|
+
params = {
|
|
420
|
+
"link_type": link_type,
|
|
421
|
+
"link_object_target": link_object_target,
|
|
422
|
+
"link_object_target_value": link_object_target_value,
|
|
423
|
+
"link_object_source": link_object_source,
|
|
424
|
+
"link_object_source_number": link_object_source_number,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
response = self._connection.session.post(self.url + "/add", json=params)
|
|
428
|
+
return self._raise_or_return_json(response)
|
|
429
|
+
|
|
430
|
+
def remove(
|
|
431
|
+
self,
|
|
432
|
+
link_object_target_value,
|
|
433
|
+
link_object_source_number,
|
|
434
|
+
link_type="normal",
|
|
435
|
+
link_object_target="Ticket",
|
|
436
|
+
link_object_source="Ticket",
|
|
437
|
+
):
|
|
438
|
+
"""Remove the Link
|
|
439
|
+
|
|
440
|
+
:params link_type: Link type ('normal', 'parent', 'child')
|
|
441
|
+
:params link_object_target: (for now: 'Ticket')
|
|
442
|
+
:params link_object_target_value: Ticket ID
|
|
443
|
+
:params link_object_source: (for now: 'Ticket')
|
|
444
|
+
:params link_object_source_number: Ticket ID
|
|
445
|
+
"""
|
|
446
|
+
params = {
|
|
447
|
+
"link_type": link_type,
|
|
448
|
+
"link_object_target": link_object_target,
|
|
449
|
+
"link_object_target_value": link_object_target_value,
|
|
450
|
+
"link_object_source": link_object_source,
|
|
451
|
+
"link_object_source_number": link_object_source_number,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
response = self._connection.session.delete(self.url + "/remove", json=params)
|
|
455
|
+
return self._raise_or_return_json(response)
|
|
456
|
+
|
|
457
|
+
def get(self, id):
|
|
458
|
+
"""Returns all the links associated with the ticket id
|
|
459
|
+
|
|
460
|
+
:param id: Ticket id
|
|
461
|
+
"""
|
|
462
|
+
params = {"link_object": "Ticket", "link_object_value": id}
|
|
463
|
+
response = self._connection.session.get(
|
|
464
|
+
self._connection.url + self.path_attribute, params=params
|
|
465
|
+
)
|
|
466
|
+
return self._raise_or_return_json(response)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class TicketArticle(Resource):
|
|
470
|
+
path_attribute = "ticket_articles"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class TicketArticleAttachment(Resource):
|
|
474
|
+
path_attribute = "ticket_attachment"
|
|
475
|
+
|
|
476
|
+
def download(self, id, article_id, ticket_id):
|
|
477
|
+
"""Download the ticket attachment associated with the ticket id
|
|
478
|
+
|
|
479
|
+
:param id: Ticket attachment id
|
|
480
|
+
:param article_id: Ticket article id
|
|
481
|
+
:param ticket_id: Ticket id
|
|
482
|
+
"""
|
|
483
|
+
response = self._connection.session.get(
|
|
484
|
+
self.url + f"/{ticket_id}/{article_id}/{id}"
|
|
485
|
+
)
|
|
486
|
+
return self._raise_or_return_json(response)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class TicketArticlePlain(Resource):
|
|
490
|
+
path_attribute = "ticket_article_plain"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class TicketPriority(Resource):
|
|
494
|
+
path_attribute = "ticket_priorities"
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class TicketState(Resource):
|
|
498
|
+
path_attribute = "ticket_states"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class User(Resource):
|
|
502
|
+
path_attribute = "users"
|
|
503
|
+
|
|
504
|
+
def me(self):
|
|
505
|
+
"""Returns current user information"""
|
|
506
|
+
response = self._connection.session.get(self.url + "/me")
|
|
507
|
+
return self._raise_or_return_json(response)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class OnlineNotification(Resource):
|
|
511
|
+
path_attribute = "online_notifications"
|
|
512
|
+
|
|
513
|
+
def mark_all_read(self):
|
|
514
|
+
"""Marks all online notification as read"""
|
|
515
|
+
response = self._connection.session.post(self.url + "/mark_all_as_read")
|
|
516
|
+
return self._raise_or_return_json(response)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class Object(Resource):
|
|
520
|
+
path_attribute = "object_manager_attributes"
|
|
521
|
+
|
|
522
|
+
def execute_migrations(self):
|
|
523
|
+
"""Executes all database migrations"""
|
|
524
|
+
response = self._connection.session.post(
|
|
525
|
+
self._connection.url + "object_manager_attributes_execute_migrations"
|
|
526
|
+
)
|
|
527
|
+
return self._raise_or_return_json(response)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class TagList(Resource):
|
|
531
|
+
"""TagList handles tags in admin scope"""
|
|
532
|
+
|
|
533
|
+
path_attribute = "tag_list"
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class TicketTag(Resource):
|
|
537
|
+
"""handles tags in the ticket scope"""
|
|
538
|
+
|
|
539
|
+
path_attribute = "tags"
|
|
540
|
+
|
|
541
|
+
def add(self, id, tag, object="Ticket"):
|
|
542
|
+
"""Add a tag to a ticket
|
|
543
|
+
|
|
544
|
+
:param id: Ticket id
|
|
545
|
+
:param tag: Tag name
|
|
546
|
+
:param object: Object to tag ((for now: 'Ticket'))
|
|
547
|
+
"""
|
|
548
|
+
|
|
549
|
+
params = {
|
|
550
|
+
"o_id": id,
|
|
551
|
+
"item": tag,
|
|
552
|
+
"object": object,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
response = self._connection.session.post(self.url + "/add", json=params)
|
|
556
|
+
return self._raise_or_return_json(response)
|
|
557
|
+
|
|
558
|
+
def remove(self, id, tag, object="Ticket"):
|
|
559
|
+
"""Remove a tag from a ticket.
|
|
560
|
+
|
|
561
|
+
:param id: Ticket id
|
|
562
|
+
:param tag: Tag name
|
|
563
|
+
:param object: Object to tag ((for now: 'Ticket'))
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
params = {
|
|
567
|
+
"o_id": id,
|
|
568
|
+
"item": tag,
|
|
569
|
+
"object": object,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
response = self._connection.session.delete(self.url + "/remove", json=params)
|
|
573
|
+
return self._raise_or_return_json(response)
|