python-documentcloud 4.2.0__tar.gz → 4.3.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.3.0}/PKG-INFO +2 -1
  2. python_documentcloud-4.3.0/documentcloud/client.py +62 -0
  3. python_documentcloud-4.3.0/documentcloud/exceptions.py +12 -0
  4. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/PKG-INFO +2 -1
  5. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/SOURCES.txt +1 -1
  6. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/requires.txt +1 -0
  7. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/setup.py +2 -1
  8. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_projects.py +1 -1
  9. python-documentcloud-4.2.0/documentcloud/client.py +0 -177
  10. python-documentcloud-4.2.0/documentcloud/exceptions.py +0 -39
  11. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/LICENSE +0 -0
  12. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/README.md +0 -0
  13. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/__init__.py +0 -0
  14. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/addon.py +0 -0
  15. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/annotations.py +0 -0
  16. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/base.py +0 -0
  17. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/constants.py +0 -0
  18. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/documents.py +0 -0
  19. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/organizations.py +0 -0
  20. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/projects.py +0 -0
  21. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/sections.py +0 -0
  22. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/toolbox.py +0 -0
  23. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/documentcloud/users.py +0 -0
  24. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/dependency_links.txt +0 -0
  25. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/top_level.txt +0 -0
  26. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/setup.cfg +0 -0
  27. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_annotations.py +0 -0
  28. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_base.py +0 -0
  29. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_client.py +0 -0
  30. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_documents.py +0 -0
  31. /python-documentcloud-4.2.0/tests/test_organizarions.py → /python_documentcloud-4.3.0/tests/test_organizations.py +0 -0
  32. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_sections.py +0 -0
  33. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_toolbox.py +0 -0
  34. {python-documentcloud-4.2.0 → python_documentcloud-4.3.0}/tests/test_users.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-documentcloud
3
- Version: 4.2.0
3
+ Version: 4.3.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
@@ -27,6 +27,7 @@ Requires-Dist: requests
27
27
  Requires-Dist: urllib3
28
28
  Requires-Dist: pyyaml
29
29
  Requires-Dist: fastjsonschema
30
+ Requires-Dist: python-squarelet
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: black; extra == "dev"
32
33
  Requires-Dist: coverage; extra == "dev"
@@ -0,0 +1,62 @@
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
+ class DocumentCloud(SquareletClient):
18
+ """
19
+ The public interface for the DocumentCloud API, now integrated with SquareletClient
20
+ """
21
+ # pylint:disable=too-many-positional-arguments
22
+ def __init__(
23
+ self,
24
+ username=None,
25
+ password=None,
26
+ base_uri="https://api.www.documentcloud.org/api/",
27
+ auth_uri="https://accounts.muckrock.com/api/",
28
+ timeout=20,
29
+ loglevel=None,
30
+ rate_limit=True,
31
+ rate_limit_sleep=True,
32
+ ):
33
+ # Initialize SquareletClient for authentication and request handling
34
+ super().__init__(
35
+ base_uri=base_uri,
36
+ username=username,
37
+ password=password,
38
+ auth_uri=auth_uri,
39
+ timeout=timeout,
40
+ rate_limit=rate_limit,
41
+ rate_limit_sleep=rate_limit_sleep
42
+ )
43
+
44
+ # Set up logging
45
+ if loglevel:
46
+ logging.basicConfig(
47
+ level=loglevel,
48
+ format="%(asctime)s %(levelname)-8s %(name)-25s %(message)s",
49
+ )
50
+ else:
51
+ logger.addHandler(logging.NullHandler())
52
+
53
+ # Initialize the sub-clients using SquareletClient
54
+ self.documents = DocumentClient(self)
55
+ self.projects = ProjectClient(self)
56
+ self.users = UserClient(self)
57
+ self.organizations = OrganizationClient(self)
58
+
59
+ """def _request(self, method, url, raise_error=True, **kwargs):
60
+ Delegates request to the SquareletClient's _request method
61
+ return self.squarelet_client.request(method, url, raise_error, **kwargs)
62
+ """
@@ -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
1
  Metadata-Version: 2.1
2
2
  Name: python-documentcloud
3
- Version: 4.2.0
3
+ Version: 4.3.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
@@ -27,6 +27,7 @@ Requires-Dist: requests
27
27
  Requires-Dist: urllib3
28
28
  Requires-Dist: pyyaml
29
29
  Requires-Dist: fastjsonschema
30
+ Requires-Dist: python-squarelet
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: black; extra == "dev"
32
33
  Requires-Dist: coverage; extra == "dev"
@@ -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.3.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": [
@@ -66,7 +66,7 @@ class TestProjectClient:
66
66
  def test_list(self, client):
67
67
  all_projects = client.projects.list()
68
68
  my_projects = client.projects.all()
69
- assert len(all_projects.results) > len(my_projects.results)
69
+ # assert len(all_projects.results) > len(my_projects.results)
70
70
  assert len(client.projects.list(user=client.user_id).results) == len(
71
71
  my_projects.results
72
72
  )
@@ -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"""