python-documentcloud 4.2.0__tar.gz → 4.4.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 (34) hide show
  1. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/PKG-INFO +13 -3
  2. python_documentcloud-4.4.0/documentcloud/client.py +58 -0
  3. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/documents.py +6 -12
  4. python_documentcloud-4.4.0/documentcloud/exceptions.py +12 -0
  5. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/python_documentcloud.egg-info/PKG-INFO +13 -3
  6. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/python_documentcloud.egg-info/SOURCES.txt +1 -1
  7. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/python_documentcloud.egg-info/requires.txt +1 -0
  8. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/setup.py +2 -2
  9. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_documents.py +1 -4
  10. python-documentcloud-4.2.0/documentcloud/client.py +0 -177
  11. python-documentcloud-4.2.0/documentcloud/exceptions.py +0 -39
  12. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/LICENSE +0 -0
  13. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/README.md +0 -0
  14. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/__init__.py +0 -0
  15. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/addon.py +0 -0
  16. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/annotations.py +0 -0
  17. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/base.py +0 -0
  18. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/constants.py +0 -0
  19. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/organizations.py +0 -0
  20. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/projects.py +0 -0
  21. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/sections.py +0 -0
  22. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/toolbox.py +0 -0
  23. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/documentcloud/users.py +0 -0
  24. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/python_documentcloud.egg-info/dependency_links.txt +0 -0
  25. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/python_documentcloud.egg-info/top_level.txt +0 -0
  26. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/setup.cfg +0 -0
  27. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_annotations.py +0 -0
  28. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_base.py +0 -0
  29. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_client.py +0 -0
  30. /python-documentcloud-4.2.0/tests/test_organizarions.py → /python_documentcloud-4.4.0/tests/test_organizations.py +0 -0
  31. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_projects.py +0 -0
  32. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_sections.py +0 -0
  33. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_toolbox.py +0 -0
  34. {python-documentcloud-4.2.0 → python_documentcloud-4.4.0}/tests/test_users.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: python-documentcloud
3
- Version: 4.2.0
3
+ Version: 4.4.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
@@ -10,7 +10,6 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3.7
14
13
  Classifier: Programming Language :: Python :: 3.8
15
14
  Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
@@ -27,6 +26,7 @@ Requires-Dist: requests
27
26
  Requires-Dist: urllib3
28
27
  Requires-Dist: pyyaml
29
28
  Requires-Dist: fastjsonschema
29
+ Requires-Dist: python-squarelet
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: black; extra == "dev"
32
32
  Requires-Dist: coverage; extra == "dev"
@@ -39,6 +39,16 @@ Requires-Dist: pytest; extra == "test"
39
39
  Requires-Dist: pytest-mock; extra == "test"
40
40
  Requires-Dist: pytest-recording; extra == "test"
41
41
  Requires-Dist: vcrpy; extra == "test"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: description-content-type
47
+ Dynamic: home-page
48
+ Dynamic: license
49
+ Dynamic: provides-extra
50
+ Dynamic: requires-dist
51
+ Dynamic: summary
42
52
 
43
53
  <pre><code> ____ _ ____ _ _
44
54
  | _ \ ___ ___ _ _ _ __ ___ ___ _ __ | |_ / ___| | ___ _ _ __| |
@@ -0,0 +1,58 @@
1
+ # Import SquareletClient from python-squarelet
2
+ # Standard Library
3
+ import logging
4
+
5
+ # Third Party
6
+ from squarelet import SquareletClient
7
+
8
+ # Local
9
+ # Local Imports
10
+ from .documents import DocumentClient
11
+ from .organizations import OrganizationClient
12
+ from .projects import ProjectClient
13
+ from .users import UserClient
14
+
15
+ logger = logging.getLogger("documentcloud")
16
+
17
+
18
+ class DocumentCloud(SquareletClient):
19
+ """
20
+ The public interface for the DocumentCloud API, now integrated with SquareletClient
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ username=None,
26
+ password=None,
27
+ base_uri="https://api.www.documentcloud.org/api/",
28
+ auth_uri="https://accounts.muckrock.com/api/",
29
+ timeout=20,
30
+ loglevel=None,
31
+ rate_limit=True,
32
+ rate_limit_sleep=True,
33
+ ):
34
+ # Initialize SquareletClient for authentication and request handling
35
+ super().__init__(
36
+ base_uri=base_uri,
37
+ username=username,
38
+ password=password,
39
+ auth_uri=auth_uri,
40
+ timeout=timeout,
41
+ rate_limit=rate_limit,
42
+ rate_limit_sleep=rate_limit_sleep,
43
+ )
44
+
45
+ # Set up logging
46
+ if loglevel:
47
+ logging.basicConfig(
48
+ level=loglevel,
49
+ format="%(asctime)s %(levelname)-8s %(name)-25s %(message)s",
50
+ )
51
+ else:
52
+ logger.addHandler(logging.NullHandler())
53
+
54
+ # Initialize the sub-clients using SquareletClient
55
+ self.documents = DocumentClient(self)
56
+ self.projects = ProjectClient(self)
57
+ self.users = UserClient(self)
58
+ self.organizations = OrganizationClient(self)
@@ -404,9 +404,7 @@ class DocumentClient(BaseAPIClient):
404
404
  path_list = self._collect_files(path, extensions)
405
405
 
406
406
  logger.info(
407
- "Upload directory on %s: Found %d files to upload",
408
- path,
409
- len(path_list)
407
+ "Upload directory on %s: Found %d files to upload", path, len(path_list)
410
408
  )
411
409
 
412
410
  # Upload all the files using the bulk API to reduce the number
@@ -444,7 +442,7 @@ class DocumentClient(BaseAPIClient):
444
442
  logger.info(
445
443
  "Error creating the following documents: %s\n%s",
446
444
  exc,
447
- "\n".join(file_paths)
445
+ "\n".join(file_paths),
448
446
  )
449
447
  continue
450
448
  else:
@@ -465,7 +463,7 @@ class DocumentClient(BaseAPIClient):
465
463
  logger.info(
466
464
  "Error uploading the following document: %s %s",
467
465
  exc,
468
- file_path
466
+ file_path,
469
467
  )
470
468
  continue
471
469
  else:
@@ -481,7 +479,7 @@ class DocumentClient(BaseAPIClient):
481
479
  logger.info(
482
480
  "Error creating the following documents: %s\n%s",
483
481
  exc,
484
- "\n".join(file_paths)
482
+ "\n".join(file_paths),
485
483
  )
486
484
  continue
487
485
  else:
@@ -504,11 +502,7 @@ class DocumentClient(BaseAPIClient):
504
502
  # Grouper will put None's on the end of the last group
505
503
  url_group = [url for url in url_group if url is not None]
506
504
 
507
- logger.info(
508
- "Uploading group %d: %s",
509
- i + 1,
510
- "\n".join(url_group)
511
- )
505
+ logger.info("Uploading group %d: %s", i + 1, "\n".join(url_group))
512
506
 
513
507
  # Create the documents
514
508
  logger.info("Creating the documents...")
@@ -531,7 +525,7 @@ class DocumentClient(BaseAPIClient):
531
525
  logger.info(
532
526
  "Error creating the following documents: %s\n%s",
533
527
  str(exc),
534
- "\n".join(url_group)
528
+ "\n".join(url_group),
535
529
  )
536
530
  continue
537
531
  else:
@@ -0,0 +1,12 @@
1
+ """
2
+ Custom exceptions for python-documentcloud
3
+ """
4
+
5
+ # pylint: disable=unused-import
6
+ # Import exceptions from python-squarelet
7
+ from squarelet.exceptions import SquareletError as DocumentCloudError
8
+ from squarelet.exceptions import DuplicateObjectError
9
+ from squarelet.exceptions import CredentialsFailedError
10
+ from squarelet.exceptions import APIError
11
+ from squarelet.exceptions import DoesNotExistError
12
+ from squarelet.exceptions import MultipleObjectsReturnedError
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: python-documentcloud
3
- Version: 4.2.0
3
+ Version: 4.4.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
@@ -10,7 +10,6 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3.7
14
13
  Classifier: Programming Language :: Python :: 3.8
15
14
  Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
@@ -27,6 +26,7 @@ Requires-Dist: requests
27
26
  Requires-Dist: urllib3
28
27
  Requires-Dist: pyyaml
29
28
  Requires-Dist: fastjsonschema
29
+ Requires-Dist: python-squarelet
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: black; extra == "dev"
32
32
  Requires-Dist: coverage; extra == "dev"
@@ -39,6 +39,16 @@ Requires-Dist: pytest; extra == "test"
39
39
  Requires-Dist: pytest-mock; extra == "test"
40
40
  Requires-Dist: pytest-recording; extra == "test"
41
41
  Requires-Dist: vcrpy; extra == "test"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: description-content-type
47
+ Dynamic: home-page
48
+ Dynamic: license
49
+ Dynamic: provides-extra
50
+ Dynamic: requires-dist
51
+ Dynamic: summary
42
52
 
43
53
  <pre><code> ____ _ ____ _ _
44
54
  | _ \ ___ ___ _ _ _ __ ___ ___ _ __ | |_ / ___| | ___ _ _ __| |
@@ -24,7 +24,7 @@ tests/test_annotations.py
24
24
  tests/test_base.py
25
25
  tests/test_client.py
26
26
  tests/test_documents.py
27
- tests/test_organizarions.py
27
+ tests/test_organizations.py
28
28
  tests/test_projects.py
29
29
  tests/test_sections.py
30
30
  tests/test_toolbox.py
@@ -6,6 +6,7 @@ requests
6
6
  urllib3
7
7
  pyyaml
8
8
  fastjsonschema
9
+ python-squarelet
9
10
 
10
11
  [dev]
11
12
  black
@@ -7,7 +7,7 @@ with open("README.md", "r") as fh:
7
7
 
8
8
  setup(
9
9
  name="python-documentcloud",
10
- version="4.2.0",
10
+ version="4.4.0",
11
11
  description="A simple Python wrapper for the DocumentCloud API",
12
12
  author="Mitchell Kotler",
13
13
  author_email="mitch@muckrock.com",
@@ -26,6 +26,7 @@ setup(
26
26
  "urllib3",
27
27
  "pyyaml",
28
28
  "fastjsonschema",
29
+ "python-squarelet",
29
30
  ),
30
31
  extras_require={
31
32
  "dev": [
@@ -48,7 +49,6 @@ setup(
48
49
  "Intended Audience :: Developers",
49
50
  "Operating System :: OS Independent",
50
51
  "License :: OSI Approved :: MIT License",
51
- "Programming Language :: Python :: 3.7",
52
52
  "Programming Language :: Python :: 3.8",
53
53
  "Programming Language :: Python :: 3.9",
54
54
  "Programming Language :: Python :: 3.10",
@@ -158,9 +158,7 @@ class TestDocument:
158
158
 
159
159
  class TestDocumentClient:
160
160
  def test_search(self, client, document):
161
- documents = client.documents.search(
162
- f"document:{document.id} simple"
163
- )
161
+ documents = client.documents.search(f"document:{document.id} simple")
164
162
  assert documents
165
163
 
166
164
  def test_list(self, client):
@@ -182,7 +180,6 @@ class TestDocumentClient:
182
180
  document = document_factory(pdf)
183
181
  assert document.status == "success"
184
182
 
185
-
186
183
  def test_upload_file_path(self, document_factory):
187
184
  document = document_factory("tests/test.pdf")
188
185
  assert document.status == "success"
@@ -1,177 +0,0 @@
1
- """
2
- The public interface for the DocumentCloud API
3
- """
4
-
5
- # Standard Library
6
- import logging
7
- from functools import partial
8
- from urllib.parse import parse_qs, urlparse
9
-
10
- # Third Party
11
- import ratelimit
12
- import requests
13
-
14
- # Local
15
- from .constants import AUTH_URI, BASE_URI, RATE_LIMIT, RATE_PERIOD, TIMEOUT
16
- from .documents import DocumentClient
17
- from .exceptions import APIError, CredentialsFailedError, DoesNotExistError
18
- from .organizations import OrganizationClient
19
- from .projects import ProjectClient
20
- from .toolbox import requests_retry_session
21
- from .users import UserClient
22
-
23
- logger = logging.getLogger("documentcloud")
24
-
25
-
26
- class DocumentCloud(object):
27
- """
28
- The public interface for the DocumentCloud API
29
- """
30
-
31
- def __init__(
32
- self,
33
- username=None,
34
- password=None,
35
- base_uri=BASE_URI,
36
- auth_uri=AUTH_URI,
37
- timeout=TIMEOUT,
38
- loglevel=None,
39
- rate_limit=True,
40
- rate_limit_sleep=True,
41
- ):
42
- self.base_uri = base_uri
43
- self.auth_uri = auth_uri
44
- self.username = username
45
- self.password = password
46
- self._user_id = None
47
- self.timeout = timeout
48
- self.refresh_token = None
49
- self.session = requests.Session()
50
- self._set_tokens()
51
-
52
- if loglevel: # pragma: no cover
53
- logging.basicConfig(
54
- level=loglevel,
55
- format="%(asctime)s %(levelname)-8s %(name)-25s %(message)s",
56
- )
57
- else:
58
- logger.addHandler(logging.NullHandler())
59
-
60
- self.documents = DocumentClient(self)
61
- self.projects = ProjectClient(self)
62
- self.users = UserClient(self)
63
- self.organizations = OrganizationClient(self)
64
-
65
- if rate_limit:
66
- self._request = ratelimit.limits(calls=RATE_LIMIT, period=RATE_PERIOD)(
67
- self._request
68
- )
69
- if rate_limit_sleep:
70
- self._request = ratelimit.sleep_and_retry(self._request)
71
-
72
- def _set_tokens(self):
73
- """Set the refresh and access tokens"""
74
- if self.refresh_token:
75
- access_token, self.refresh_token = self._refresh_tokens(self.refresh_token)
76
- elif self.username and self.password:
77
- access_token, self.refresh_token = self._get_tokens(
78
- self.username, self.password
79
- )
80
- else:
81
- access_token = None
82
-
83
- if access_token:
84
- self.session.headers.update({"Authorization": f"Bearer {access_token}"})
85
-
86
- def _get_tokens(self, username, password):
87
- """Get an access and refresh token in exchange for the username and password"""
88
- response = requests_retry_session().post(
89
- f"{self.auth_uri}token/",
90
- json={"username": username, "password": password},
91
- timeout=self.timeout,
92
- )
93
-
94
- if response.status_code == requests.codes.UNAUTHORIZED:
95
- raise CredentialsFailedError("The username and password are incorrect")
96
-
97
- self.raise_for_status(response)
98
-
99
- json = response.json()
100
- return (json["access"], json["refresh"])
101
-
102
- def _refresh_tokens(self, refresh_token):
103
- """Refresh the access and refresh tokens"""
104
- response = requests_retry_session().post(
105
- f"{self.auth_uri}refresh/",
106
- json={"refresh": refresh_token},
107
- timeout=self.timeout,
108
- )
109
-
110
- if response.status_code == requests.codes.UNAUTHORIZED:
111
- # refresh token is expired
112
- return self._get_tokens(self.username, self.password)
113
-
114
- self.raise_for_status(response)
115
-
116
- json = response.json()
117
- return (json["access"], json["refresh"])
118
-
119
- @property
120
- def user_id(self):
121
- if self._user_id is None:
122
- user = self.users.get("me")
123
- self._user_id = user.id
124
- return self._user_id
125
-
126
- def _request(self, method, url, raise_error=True, **kwargs):
127
- """Generic method to make API requests"""
128
- # pylint: disable=method-hidden
129
- logger.info("request: %s - %s - %s", method, url, kwargs)
130
- set_tokens = kwargs.pop("set_tokens", True)
131
- full_url = kwargs.pop("full_url", False)
132
-
133
- if not full_url:
134
- url = f"{self.base_uri}{url}"
135
-
136
- # set the API to version 2.0
137
- parsed_url = urlparse(url)
138
- if "version" not in parse_qs(parsed_url.query):
139
- # check to avoid double setting version
140
- kwargs.setdefault("params", {}).update({"version": "2.0"})
141
-
142
- response = requests_retry_session(session=self.session).request(
143
- method, url, timeout=self.timeout, **kwargs
144
- )
145
- logger.debug("response: %s - %s", response.status_code, response.content)
146
- if (
147
- response.status_code in [requests.codes.FORBIDDEN, requests.codes.TOO_MANY]
148
- and set_tokens
149
- ):
150
- self._set_tokens()
151
- # track set_tokens to not enter an infinite loop
152
- kwargs["set_tokens"] = False
153
- return self._request(method, url, full_url=True, **kwargs)
154
-
155
- if raise_error:
156
- self.raise_for_status(response)
157
-
158
- return response
159
-
160
- def __getattr__(self, attr):
161
- """Generate methods for each HTTP request type"""
162
- methods = ["get", "options", "head", "post", "put", "patch", "delete"]
163
- if attr in methods:
164
- return partial(self._request, attr)
165
- raise AttributeError(
166
- f"'{self.__class__.__name__}' object has no attribute '{attr}'"
167
- )
168
-
169
- def raise_for_status(self, response):
170
- """Raise for status with a custom error class"""
171
- try:
172
- response.raise_for_status()
173
- except requests.exceptions.RequestException as exc:
174
- if exc.response.status_code == 404:
175
- raise DoesNotExistError(response=exc.response) from exc
176
- else:
177
- raise APIError(response=exc.response) from exc
@@ -1,39 +0,0 @@
1
- """
2
- Custom exceptions for python-documentcloud
3
- """
4
-
5
-
6
- class DocumentCloudError(Exception):
7
- """Base class for errors for python-documentcloud"""
8
-
9
- def __init__(self, *args, **kwargs):
10
- self.response = kwargs.pop("response", None)
11
- if self.response is not None:
12
- self.error = self.response.text
13
- self.status_code = self.response.status_code
14
- if not args:
15
- args = [f"{self.status_code} - {self.error}"]
16
- else:
17
- self.error = None
18
- self.status_code = None
19
- super().__init__(*args, **kwargs)
20
-
21
-
22
- class DuplicateObjectError(DocumentCloudError):
23
- """Raised when an object is added to a unique list more than once"""
24
-
25
-
26
- class CredentialsFailedError(DocumentCloudError):
27
- """Raised if unable to obtain an access token due to bad login credentials"""
28
-
29
-
30
- class APIError(DocumentCloudError):
31
- """Any other error calling the API"""
32
-
33
-
34
- class DoesNotExistError(APIError):
35
- """Raised when the user asks the API for something it cannot find"""
36
-
37
-
38
- class MultipleObjectsReturnedError(APIError):
39
- """Raised when the API returns multiple objects when it expected one"""