python-documentcloud 3.7.1__tar.gz → 4.0.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.
Files changed (25) hide show
  1. {python-documentcloud-3.7.1/python_documentcloud.egg-info → python-documentcloud-4.0.0}/PKG-INFO +13 -16
  2. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/README.md +8 -10
  3. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/addon.py +33 -3
  4. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/annotations.py +3 -8
  5. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/base.py +21 -40
  6. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/client.py +8 -13
  7. python-documentcloud-4.0.0/documentcloud/constants.py +101 -0
  8. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/documents.py +80 -74
  9. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/exceptions.py +2 -4
  10. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/organizations.py +0 -7
  11. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/projects.py +11 -22
  12. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/sections.py +4 -11
  13. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/toolbox.py +4 -9
  14. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/users.py +0 -7
  15. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0/python_documentcloud.egg-info}/PKG-INFO +13 -16
  16. python-documentcloud-4.0.0/setup.py +59 -0
  17. python-documentcloud-3.7.1/documentcloud/constants.py +0 -104
  18. python-documentcloud-3.7.1/setup.py +0 -60
  19. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/LICENSE +0 -0
  20. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/documentcloud/__init__.py +0 -0
  21. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/python_documentcloud.egg-info/SOURCES.txt +0 -0
  22. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/python_documentcloud.egg-info/dependency_links.txt +0 -0
  23. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/python_documentcloud.egg-info/requires.txt +2 -2
  24. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/python_documentcloud.egg-info/top_level.txt +0 -0
  25. {python-documentcloud-3.7.1 → python-documentcloud-4.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-documentcloud
3
- Version: 3.7.1
3
+ Version: 4.0.0
4
4
  Summary: A simple Python wrapper for the DocumentCloud API
5
5
  Home-page: https://github.com/muckrock/python-documentcloud
6
6
  Author: Mitchell Kotler
@@ -11,13 +11,12 @@ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 2
16
- Classifier: Programming Language :: Python :: 2.7
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.6
19
14
  Classifier: Programming Language :: Python :: 3.7
20
15
  Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
21
20
  Classifier: Topic :: Internet :: WWW/HTTP
22
21
  Description-Content-Type: text/markdown
23
22
  Provides-Extra: dev
@@ -32,19 +31,17 @@ License-File: LICENSE
32
31
 
33
32
  A simple python wrapper for the DocumentCloud API
34
33
 
35
- * Documentation: [http://documentcloud.readthedocs.org/](http://documentcloud.readthedocs.org/)
36
- * Issues: [https://github.com/muckrock/python-documentcloud/issues](https://github.com/muckrock/python-documentcloud/issues)
37
- * Packaging: [https://pypi.python.org/pypi/python-documentcloud](https://pypi.python.org/pypi/python-documentcloud)
34
+ - Documentation: [http://documentcloud.readthedocs.org/](http://documentcloud.readthedocs.org/)
35
+ - Issues: [https://github.com/muckrock/python-documentcloud/issues](https://github.com/muckrock/python-documentcloud/issues)
36
+ - Packaging: [https://pypi.python.org/pypi/python-documentcloud](https://pypi.python.org/pypi/python-documentcloud)
38
37
 
39
- Features
40
- --------
38
+ ## Features
41
39
 
42
- * Retrieve and edit documents and projects, both public and private, from documentcloud.org
43
- * Upload PDFs into your documentcloud.org account and organize them into projects
44
- * Download text and images extracted from your PDFs by DocumentCloud
40
+ - Retrieve and edit documents and projects, both public and private, from documentcloud.org
41
+ - Upload PDFs into your documentcloud.org account and organize them into projects
42
+ - Download text and images extracted from your PDFs by DocumentCloud
45
43
 
46
- Getting started
47
- ---------------
44
+ ## Getting started
48
45
 
49
46
  Installation is as easy as...
50
47
 
@@ -6,19 +6,17 @@
6
6
 
7
7
  A simple python wrapper for the DocumentCloud API
8
8
 
9
- * Documentation: [http://documentcloud.readthedocs.org/](http://documentcloud.readthedocs.org/)
10
- * Issues: [https://github.com/muckrock/python-documentcloud/issues](https://github.com/muckrock/python-documentcloud/issues)
11
- * Packaging: [https://pypi.python.org/pypi/python-documentcloud](https://pypi.python.org/pypi/python-documentcloud)
9
+ - Documentation: [http://documentcloud.readthedocs.org/](http://documentcloud.readthedocs.org/)
10
+ - Issues: [https://github.com/muckrock/python-documentcloud/issues](https://github.com/muckrock/python-documentcloud/issues)
11
+ - Packaging: [https://pypi.python.org/pypi/python-documentcloud](https://pypi.python.org/pypi/python-documentcloud)
12
12
 
13
- Features
14
- --------
13
+ ## Features
15
14
 
16
- * Retrieve and edit documents and projects, both public and private, from documentcloud.org
17
- * Upload PDFs into your documentcloud.org account and organize them into projects
18
- * Download text and images extracted from your PDFs by DocumentCloud
15
+ - Retrieve and edit documents and projects, both public and private, from documentcloud.org
16
+ - Upload PDFs into your documentcloud.org account and organize them into projects
17
+ - Download text and images extracted from your PDFs by DocumentCloud
19
18
 
20
- Getting started
21
- ---------------
19
+ ## Getting started
22
20
 
23
21
  Installation is as easy as...
24
22
 
@@ -24,7 +24,7 @@ class BaseAddOn:
24
24
 
25
25
  def __init__(self):
26
26
  args = self._parse_arguments()
27
- client = self._create_client(args)
27
+ self._create_client(args)
28
28
 
29
29
  # a unique identifier for this run
30
30
  self.id = args.pop("id", None)
@@ -42,6 +42,8 @@ class BaseAddOn:
42
42
  self.org_id = args.pop("organization", None)
43
43
  # add on specific data
44
44
  self.data = args.pop("data", None)
45
+ # title of the addon
46
+ self.title = args.pop("title", None)
45
47
 
46
48
  def _create_client(self, args):
47
49
  client_kwargs = {
@@ -63,7 +65,7 @@ class BaseAddOn:
63
65
  self.client.refresh_token = args["refresh_token"]
64
66
  if args["token"] is not None:
65
67
  self.client.session.headers.update(
66
- {"Authorization": "Bearer {}".format(args["token"])}
68
+ {"Authorization": f"Bearer {args['token']}"}
67
69
  )
68
70
 
69
71
  # custom user agent for AddOns
@@ -117,9 +119,11 @@ class BaseAddOn:
117
119
 
118
120
  # validate parameter data
119
121
  try:
120
- with open("config.yaml") as config:
122
+ with open("config.yaml", encoding="utf-8") as config:
121
123
  schema = yaml.safe_load(config)
122
124
  args["data"] = fastjsonschema.validate(schema, args["data"])
125
+ # add title in case the add-on wants to reference its own title
126
+ args["title"] = schema.get("title")
123
127
  except FileNotFoundError:
124
128
  pass
125
129
  except fastjsonschema.JsonSchemaException as exc:
@@ -171,6 +175,7 @@ class AddOn(BaseAddOn):
171
175
  else:
172
176
  # text file's buffer is in binary mode
173
177
  data = file.buffer
178
+ # pylint: disable=W3101
174
179
  response = requests.put(presigned_url, data=data)
175
180
  response.raise_for_status()
176
181
  return self.client.patch(
@@ -203,6 +208,8 @@ class AddOn(BaseAddOn):
203
208
  documents = self.client.documents.search(self.query)
204
209
  return documents.count
205
210
 
211
+ return 0
212
+
206
213
  def get_documents(self):
207
214
  """Get documents from either selected or queried documents"""
208
215
  if self.documents:
@@ -214,6 +221,29 @@ class AddOn(BaseAddOn):
214
221
 
215
222
  yield from documents
216
223
 
224
+ def charge_credits(self, amount):
225
+ """Charge the organization a certain amount of premium credits"""
226
+
227
+ if not self.id:
228
+ print(f"Charge credits: {amount}")
229
+ return None
230
+ elif not self.org_id:
231
+ self.set_message("No organization to charge.")
232
+ raise ValueError
233
+
234
+ resp = self.client.post(
235
+ f"organizations/{self.org_id}/ai_credits/",
236
+ json={
237
+ "ai_credits": amount,
238
+ "addonrun_id": self.id,
239
+ "note": f"AddOn run: {self.title} - {self.id}",
240
+ },
241
+ )
242
+ if resp.status_code != 200:
243
+ self.set_message("Error charging AI credits.")
244
+ raise ValueError
245
+ return resp
246
+
217
247
 
218
248
  class CronAddOn(BaseAddOn):
219
249
  """DEPREACTED"""
@@ -1,8 +1,4 @@
1
- # Future
2
- from __future__ import division, print_function, unicode_literals
3
-
4
1
  # Third Party
5
- from future.utils import python_2_unicode_compatible
6
2
  from listcrunch.listcrunch import uncrunch
7
3
 
8
4
  # Local
@@ -10,7 +6,6 @@ from .base import BaseAPIObject, ChildAPIClient
10
6
  from .toolbox import merge_dicts
11
7
 
12
8
 
13
- @python_2_unicode_compatible
14
9
  class Annotation(BaseAPIObject):
15
10
  """A note on a document"""
16
11
 
@@ -30,7 +25,7 @@ class Annotation(BaseAPIObject):
30
25
 
31
26
  @property
32
27
  def api_path(self):
33
- return "documents/{}/notes".format(self.document.id)
28
+ return f"documents/{self.document.id}/notes"
34
29
 
35
30
  @property
36
31
  def location(self):
@@ -71,7 +66,7 @@ class AnnotationClient(ChildAPIClient):
71
66
 
72
67
  @property
73
68
  def api_path(self):
74
- return "documents/{}/notes".format(self.parent.id)
69
+ return f"documents/{self.parent.id}/notes"
75
70
 
76
71
  def create(
77
72
  self,
@@ -102,7 +97,7 @@ class AnnotationClient(ChildAPIClient):
102
97
  "x2": x2,
103
98
  "y2": y2,
104
99
  }
105
- response = self.client.post(self.api_path + "/", json=data)
100
+ response = self.client.post(f"{self.api_path}/", json=data)
106
101
  return Annotation(
107
102
  self.client, merge_dicts(response.json(), {"document": self.parent})
108
103
  )
@@ -1,20 +1,14 @@
1
- # Future
2
- from __future__ import division, print_function, unicode_literals
3
-
4
1
  # Standard Library
5
- from builtins import str
6
2
  from copy import copy
7
3
 
8
4
  # Third Party
9
5
  from dateutil.parser import parse as dateparser
10
- from future.utils import python_2_unicode_compatible
11
6
 
12
7
  # Local
13
8
  from .exceptions import DuplicateObjectError
14
9
  from .toolbox import get_id, merge_dicts
15
10
 
16
11
 
17
- @python_2_unicode_compatible
18
12
  class APIResults(object):
19
13
  """Class for encapsulating paginated list results from the API"""
20
14
 
@@ -39,10 +33,10 @@ class APIResults(object):
39
33
  ]
40
34
 
41
35
  def __repr__(self):
42
- return "<APIResults: {!r}".format(self.results) # pragma: no cover
36
+ return f"<APIResults: {self.results!r}>" # pragma: no cover
43
37
 
44
38
  def __str__(self):
45
- return "[{}]".format(", ".join(str(r) for r in self.results))
39
+ return f"[{', '.join(str(r) for r in self.results)}]"
46
40
 
47
41
  def __getitem__(self, key):
48
42
  # pylint: disable=unsubscriptable-object
@@ -104,21 +98,19 @@ class BaseAPIClient(object):
104
98
  params = {"expand": ",".join(expand)}
105
99
  else:
106
100
  params = {}
107
- response = self.client.get(
108
- "{}/{}/".format(self.api_path, get_id(id_)), params=params
109
- )
101
+ response = self.client.get(f"{self.api_path}/{get_id(id_)}/", params=params)
110
102
  # pylint: disable=not-callable
111
103
  return self.resource(self.client, response.json())
112
104
 
113
105
  def delete(self, id_):
114
106
  """Deletes a resource"""
115
- self.client.delete("{}/{}/".format(self.api_path, get_id(id_)))
107
+ self.client.delete(f"{self.api_path}/{get_id(id_)}")
116
108
 
117
109
  def all(self, **params):
118
110
  return self.list(**params)
119
111
 
120
112
  def list(self, **params):
121
- response = self.client.get(self.api_path + "/", params=params)
113
+ response = self.client.get(f"{self.api_path}/", params=params)
122
114
  return APIResults(self.resource, self.client, response)
123
115
 
124
116
 
@@ -126,11 +118,11 @@ class ChildAPIClient(BaseAPIClient):
126
118
  """Base client for sub resources"""
127
119
 
128
120
  def __init__(self, client, parent):
129
- super(ChildAPIClient, self).__init__(client)
121
+ super().__init__(client)
130
122
  self.parent = parent
131
123
 
132
124
  def list(self, **params):
133
- response = self.client.get(self.api_path + "/", params=params)
125
+ response = self.client.get(f"{self.api_path}/", params=params)
134
126
  parent_name = self.parent.__class__.__name__.lower()
135
127
  return APIResults(
136
128
  self.resource, self.client, response, {parent_name: self.parent}
@@ -156,9 +148,7 @@ class BaseAPIObject(object):
156
148
  setattr(self, field, dateparser(getattr(self, field)))
157
149
 
158
150
  def __repr__(self):
159
- return "<{}: {} - {}>".format(
160
- self.__class__.__name__, self.id, self
161
- ) # pragma: no cover
151
+ return f"<{self.__class__.__name__}: {self.id} - {self}>" # pragma: no cover
162
152
 
163
153
  def __eq__(self, obj):
164
154
  return isinstance(obj, type(self)) and self.id == obj.id
@@ -169,65 +159,56 @@ class BaseAPIObject(object):
169
159
 
170
160
  def save(self):
171
161
  data = {f: getattr(self, f) for f in self.writable_fields if hasattr(self, f)}
172
- self._client.put("{}/{}/".format(self.api_path, self.id), json=data)
162
+ self._client.put(f"{self.api_path}/{self.id}/", json=data)
173
163
 
174
164
  def delete(self):
175
- self._client.delete("{}/{}/".format(self.api_path, self.id))
165
+ self._client.delete(f"{self.api_path}/{self.id}")
176
166
 
177
167
 
178
- @python_2_unicode_compatible
179
168
  class APISet(list):
180
169
  def __init__(self, iterable, resource):
181
- super(APISet, self).__init__(iterable)
170
+ super().__init__(iterable)
182
171
  self.resource = resource
183
172
  if not all(isinstance(obj, self.resource) for obj in self):
184
173
  raise TypeError(
185
- "Only {} can be added to this list".format(
186
- self.resource.__class__.__name__
187
- )
174
+ f"Only {self.resource.__class__.__name__} can be added to this list"
188
175
  )
189
176
  ids = [obj.id for obj in self]
190
177
  for id_ in ids:
191
178
  if ids.count(id_) > 1:
192
179
  raise DuplicateObjectError(
193
- "Object with ID {} appears in the list more than once".format(id_)
180
+ f"Object with ID {id_} appears in the list more than once"
194
181
  )
195
182
 
196
183
  def append(self, obj):
197
184
  if not isinstance(obj, self.resource):
198
185
  raise TypeError(
199
- "Only {} can be added to this list".format(
200
- self.resource.__class__.__name__
201
- )
186
+ f"Only {self.resource.__class__.__name__} can be added to this list"
202
187
  )
203
188
  if obj.id in [i.id for i in self]:
204
189
  raise DuplicateObjectError(
205
- "Object with ID {} appears in the list more than once".format(obj.id)
190
+ f"Object with ID {obj.id} appears in the list more than once"
206
191
  )
207
- super(APISet, self).append(copy(obj))
192
+ super().append(copy(obj))
208
193
 
209
194
  def add(self, obj):
210
195
  if not isinstance(obj, self.resource):
211
196
  raise TypeError(
212
- "Only {} can be added to this list".format(
213
- self.resource.__class__.__name__
214
- )
197
+ f"Only {self.resource.__class__.__name__} can be added to this list"
215
198
  )
216
199
  # skip duplicates silently
217
200
  if obj.id not in [i.id for i in self]:
218
- super(APISet, self).append(copy(obj))
201
+ super().append(copy(obj))
219
202
 
220
203
  def extend(self, list_):
221
204
  if not all(isinstance(obj, self.resource) for obj in list_):
222
205
  raise TypeError(
223
- "Only {} can be added to this list".format(
224
- self.resource.__class__.__name__
225
- )
206
+ f"Only {self.resource.__class__.__name__} can be added to this list"
226
207
  )
227
208
  ids = [obj.id for obj in self + list_]
228
209
  for id_ in ids:
229
210
  if ids.count(id_) > 1:
230
211
  raise DuplicateObjectError(
231
- "Object with ID {} appears in the list more than once".format(id)
212
+ f"Object with ID {id_} appears in the list more than once"
232
213
  )
233
- super(APISet, self).extend(copy(obj) for obj in list_)
214
+ super().extend(copy(obj) for obj in list_)
@@ -2,9 +2,6 @@
2
2
  The public interface for the DocumentCloud API
3
3
  """
4
4
 
5
- # Future
6
- from __future__ import division, print_function, unicode_literals
7
-
8
5
  # Standard Library
9
6
  import logging
10
7
  from functools import partial
@@ -84,20 +81,18 @@ class DocumentCloud(object):
84
81
  access_token = None
85
82
 
86
83
  if access_token:
87
- self.session.headers.update(
88
- {"Authorization": "Bearer {}".format(access_token)}
89
- )
84
+ self.session.headers.update({"Authorization": f"Bearer {access_token}"})
90
85
 
91
86
  def _get_tokens(self, username, password):
92
87
  """Get an access and refresh token in exchange for the username and password"""
93
88
  response = requests_retry_session().post(
94
- "{}token/".format(self.auth_uri),
89
+ f"{self.auth_uri}token/",
95
90
  json={"username": username, "password": password},
96
91
  timeout=self.timeout,
97
92
  )
98
93
 
99
94
  if response.status_code == requests.codes.UNAUTHORIZED:
100
- raise CredentialsFailedError("The username and password is incorrect")
95
+ raise CredentialsFailedError("The username and password are incorrect")
101
96
 
102
97
  self.raise_for_status(response)
103
98
 
@@ -107,7 +102,7 @@ class DocumentCloud(object):
107
102
  def _refresh_tokens(self, refresh_token):
108
103
  """Refresh the access and refresh tokens"""
109
104
  response = requests_retry_session().post(
110
- "{}refresh/".format(self.auth_uri),
105
+ f"{self.auth_uri}refresh/",
111
106
  json={"refresh": refresh_token},
112
107
  timeout=self.timeout,
113
108
  )
@@ -136,7 +131,7 @@ class DocumentCloud(object):
136
131
  full_url = kwargs.pop("full_url", False)
137
132
 
138
133
  if not full_url:
139
- url = "{}{}".format(self.base_uri, url)
134
+ url = f"{self.base_uri}{url}"
140
135
 
141
136
  # set the API to version 2.0
142
137
  parsed_url = urlparse(url)
@@ -165,7 +160,7 @@ class DocumentCloud(object):
165
160
  if attr in methods:
166
161
  return partial(self._request, attr)
167
162
  raise AttributeError(
168
- "'{}' object has no attribute '{}'".format(self.__class__.__name__, attr)
163
+ f"'{self.__class__.__name__}' object has no attribute '{attr}'"
169
164
  )
170
165
 
171
166
  def raise_for_status(self, response):
@@ -174,6 +169,6 @@ class DocumentCloud(object):
174
169
  response.raise_for_status()
175
170
  except requests.exceptions.RequestException as exc:
176
171
  if exc.response.status_code == 404:
177
- raise DoesNotExistError(response=exc.response)
172
+ raise DoesNotExistError(response=exc.response) from exc
178
173
  else:
179
- raise APIError(response=exc.response)
174
+ raise APIError(response=exc.response) from exc
@@ -0,0 +1,101 @@
1
+ PER_PAGE_MAX = 100
2
+ BULK_LIMIT = 25
3
+ BASE_URI = "https://api.www.documentcloud.org/api/"
4
+ AUTH_URI = "https://accounts.muckrock.com/api/"
5
+ TIMEOUT = 20
6
+ RATE_LIMIT = 10
7
+ RATE_PERIOD = 1
8
+ SUPPORTED_EXTENSIONS = [
9
+ ".abw",
10
+ ".zabw",
11
+ ".md",
12
+ ".pm3",
13
+ ".pm4",
14
+ ".pm5",
15
+ ".pm6",
16
+ ".p65",
17
+ ".cwk",
18
+ ".agd",
19
+ ".fhd",
20
+ ".kth",
21
+ ".key",
22
+ ".numbers",
23
+ ".pages",
24
+ ".bmp",
25
+ ".csv",
26
+ ".txt",
27
+ ".cdr",
28
+ ".cmx",
29
+ ".cgm",
30
+ ".dif",
31
+ ".dbf",
32
+ ".xml",
33
+ ".eps",
34
+ ".emf",
35
+ ".fb2",
36
+ ".gnm",
37
+ ".gnumeric",
38
+ ".gif",
39
+ ".hwp",
40
+ ".plt",
41
+ ".html",
42
+ ".htm",
43
+ ".jtd",
44
+ ".jtt",
45
+ ".jpg",
46
+ ".jpeg",
47
+ ".wk1",
48
+ ".wks",
49
+ ".123",
50
+ ".wk3",
51
+ ".wk4",
52
+ ".pct",
53
+ ".mml",
54
+ ".xls",
55
+ ".xlw",
56
+ ".xlt",
57
+ ".xlsx",
58
+ ".docx",
59
+ ".pptx",
60
+ ".ppt",
61
+ ".pps",
62
+ ".pot",
63
+ ".pptx",
64
+ ".pub",
65
+ ".rtf",
66
+ ".xml",
67
+ ".doc",
68
+ ".dot",
69
+ ".docx",
70
+ ".wps",
71
+ ".wks",
72
+ ".wdb",
73
+ ".wri",
74
+ ".vsd",
75
+ ".pgm",
76
+ ".pbm",
77
+ ".ppm",
78
+ ".odt",
79
+ ".fodt",
80
+ ".ods",
81
+ ".fods",
82
+ ".odp",
83
+ ".fodp",
84
+ ".odg",
85
+ ".fodg",
86
+ ".odf",
87
+ ".odb",
88
+ ".sxw",
89
+ ".stw",
90
+ ".sxc",
91
+ ".stc",
92
+ ".sxi",
93
+ ".sti",
94
+ ".sxd",
95
+ ".std",
96
+ ".sxm",
97
+ ".pcx",
98
+ ".pcd",
99
+ ".psd",
100
+ ".pdf",
101
+ ]