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.
@@ -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.
@@ -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,7 @@
1
+ """Top-level package for Zammad API Client."""
2
+
3
+ __author__ = """Joe Paul"""
4
+ __email__ = "joeirimpan@gmail.com"
5
+ __version__ = "3.2.0"
6
+
7
+ from .api import ZammadAPI # noqa: F401
@@ -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)
@@ -0,0 +1,2 @@
1
+ class ConfigException(Exception):
2
+ pass