python-documentcloud 4.1.3__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.1.3 → python_documentcloud-4.3.0}/PKG-INFO +21 -5
  2. python_documentcloud-4.3.0/documentcloud/client.py +62 -0
  3. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/constants.py +42 -1
  4. python_documentcloud-4.3.0/documentcloud/exceptions.py +12 -0
  5. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/PKG-INFO +21 -5
  6. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/SOURCES.txt +10 -1
  7. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/requires.txt +3 -2
  8. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/setup.py +2 -1
  9. python_documentcloud-4.3.0/tests/test_annotations.py +50 -0
  10. python_documentcloud-4.3.0/tests/test_base.py +114 -0
  11. python_documentcloud-4.3.0/tests/test_client.py +113 -0
  12. python_documentcloud-4.3.0/tests/test_documents.py +238 -0
  13. python_documentcloud-4.3.0/tests/test_organizations.py +11 -0
  14. python_documentcloud-4.3.0/tests/test_projects.py +111 -0
  15. python_documentcloud-4.3.0/tests/test_sections.py +32 -0
  16. python_documentcloud-4.3.0/tests/test_toolbox.py +25 -0
  17. python_documentcloud-4.3.0/tests/test_users.py +10 -0
  18. python-documentcloud-4.1.3/documentcloud/client.py +0 -177
  19. python-documentcloud-4.1.3/documentcloud/exceptions.py +0 -39
  20. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/LICENSE +0 -0
  21. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/README.md +0 -0
  22. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/__init__.py +0 -0
  23. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/addon.py +0 -0
  24. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/annotations.py +0 -0
  25. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/base.py +0 -0
  26. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/documents.py +0 -0
  27. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/organizations.py +0 -0
  28. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/projects.py +0 -0
  29. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/sections.py +0 -0
  30. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/toolbox.py +0 -0
  31. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/users.py +0 -0
  32. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/dependency_links.txt +0 -0
  33. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/top_level.txt +0 -0
  34. {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/setup.cfg +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-documentcloud
3
- Version: 4.1.3
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
7
7
  Author-email: mitch@muckrock.com
8
8
  License: MIT
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Intended Audience :: Developers
12
11
  Classifier: Operating System :: OS Independent
@@ -19,9 +18,28 @@ Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Topic :: Internet :: WWW/HTTP
21
20
  Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: future
23
+ Requires-Dist: listcrunch>=1.0.1
24
+ Requires-Dist: python-dateutil
25
+ Requires-Dist: ratelimit
26
+ Requires-Dist: requests
27
+ Requires-Dist: urllib3
28
+ Requires-Dist: pyyaml
29
+ Requires-Dist: fastjsonschema
30
+ Requires-Dist: python-squarelet
22
31
  Provides-Extra: dev
32
+ Requires-Dist: black; extra == "dev"
33
+ Requires-Dist: coverage; extra == "dev"
34
+ Requires-Dist: isort; extra == "dev"
35
+ Requires-Dist: pylint; extra == "dev"
36
+ Requires-Dist: sphinx; extra == "dev"
37
+ Requires-Dist: twine; extra == "dev"
23
38
  Provides-Extra: test
24
- License-File: LICENSE
39
+ Requires-Dist: pytest; extra == "test"
40
+ Requires-Dist: pytest-mock; extra == "test"
41
+ Requires-Dist: pytest-recording; extra == "test"
42
+ Requires-Dist: vcrpy; extra == "test"
25
43
 
26
44
  <pre><code> ____ _ ____ _ _
27
45
  | _ \ ___ ___ _ _ _ __ ___ ___ _ __ | |_ / ___| | ___ _ _ __| |
@@ -48,5 +66,3 @@ Installation is as easy as...
48
66
  ```bash
49
67
  $ pip install python-documentcloud
50
68
  ```
51
-
52
-
@@ -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
+ """
@@ -8,7 +8,7 @@ RATE_PERIOD = 1
8
8
  SUPPORTED_EXTENSIONS = [
9
9
  ".abw",
10
10
  ".zabw",
11
- ".md",
11
+ ".pmd",
12
12
  ".pm3",
13
13
  ".pm4",
14
14
  ".pm5",
@@ -51,11 +51,16 @@ SUPPORTED_EXTENSIONS = [
51
51
  ".wk4",
52
52
  ".pct",
53
53
  ".mml",
54
+ ".xml",
55
+ ".xls",
56
+ ".xlw",
57
+ ".xlt",
54
58
  ".xls",
55
59
  ".xlw",
56
60
  ".xlt",
57
61
  ".xlsx",
58
62
  ".docx",
63
+ ".xlsx",
59
64
  ".pptx",
60
65
  ".ppt",
61
66
  ".pps",
@@ -97,5 +102,41 @@ SUPPORTED_EXTENSIONS = [
97
102
  ".pcx",
98
103
  ".pcd",
99
104
  ".psd",
105
+ ".txt",
100
106
  ".pdf",
107
+ ".png",
108
+ ".qxp",
109
+ ".wb2",
110
+ ".wq1",
111
+ ".wq2",
112
+ ".svg",
113
+ ".sgv",
114
+ ".602",
115
+ ".txt",
116
+ ".sdc",
117
+ ".vor",
118
+ ".sda",
119
+ ".sdd",
120
+ ".sdp",
121
+ ".vor",
122
+ ".sdw",
123
+ ".sgl",
124
+ ".vor",
125
+ ".sgf",
126
+ ".rlf",
127
+ ".ras",
128
+ ".svm",
129
+ ".slk",
130
+ ".tif",
131
+ ".tiff",
132
+ ".tga",
133
+ ".uof",
134
+ ".uot",
135
+ ".uos",
136
+ ".uop",
137
+ ".wpd",
138
+ ".wps",
139
+ ".xbm",
140
+ ".xpm",
141
+ ".zmf",
101
142
  ]
@@ -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,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-documentcloud
3
- Version: 4.1.3
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
7
7
  Author-email: mitch@muckrock.com
8
8
  License: MIT
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Intended Audience :: Developers
12
11
  Classifier: Operating System :: OS Independent
@@ -19,9 +18,28 @@ Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Topic :: Internet :: WWW/HTTP
21
20
  Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: future
23
+ Requires-Dist: listcrunch>=1.0.1
24
+ Requires-Dist: python-dateutil
25
+ Requires-Dist: ratelimit
26
+ Requires-Dist: requests
27
+ Requires-Dist: urllib3
28
+ Requires-Dist: pyyaml
29
+ Requires-Dist: fastjsonschema
30
+ Requires-Dist: python-squarelet
22
31
  Provides-Extra: dev
32
+ Requires-Dist: black; extra == "dev"
33
+ Requires-Dist: coverage; extra == "dev"
34
+ Requires-Dist: isort; extra == "dev"
35
+ Requires-Dist: pylint; extra == "dev"
36
+ Requires-Dist: sphinx; extra == "dev"
37
+ Requires-Dist: twine; extra == "dev"
23
38
  Provides-Extra: test
24
- License-File: LICENSE
39
+ Requires-Dist: pytest; extra == "test"
40
+ Requires-Dist: pytest-mock; extra == "test"
41
+ Requires-Dist: pytest-recording; extra == "test"
42
+ Requires-Dist: vcrpy; extra == "test"
25
43
 
26
44
  <pre><code> ____ _ ____ _ _
27
45
  | _ \ ___ ___ _ _ _ __ ___ ___ _ __ | |_ / ___| | ___ _ _ __| |
@@ -48,5 +66,3 @@ Installation is as easy as...
48
66
  ```bash
49
67
  $ pip install python-documentcloud
50
68
  ```
51
-
52
-
@@ -19,4 +19,13 @@ python_documentcloud.egg-info/PKG-INFO
19
19
  python_documentcloud.egg-info/SOURCES.txt
20
20
  python_documentcloud.egg-info/dependency_links.txt
21
21
  python_documentcloud.egg-info/requires.txt
22
- python_documentcloud.egg-info/top_level.txt
22
+ python_documentcloud.egg-info/top_level.txt
23
+ tests/test_annotations.py
24
+ tests/test_base.py
25
+ tests/test_client.py
26
+ tests/test_documents.py
27
+ tests/test_organizations.py
28
+ tests/test_projects.py
29
+ tests/test_sections.py
30
+ tests/test_toolbox.py
31
+ tests/test_users.py
@@ -1,11 +1,12 @@
1
- fastjsonschema
2
1
  future
3
2
  listcrunch>=1.0.1
4
3
  python-dateutil
5
- pyyaml
6
4
  ratelimit
7
5
  requests
8
6
  urllib3
7
+ pyyaml
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.1.3",
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": [
@@ -0,0 +1,50 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+
7
+ # Third Party
8
+ import pytest
9
+
10
+ # DocumentCloud
11
+ from documentcloud.annotations import Annotation
12
+
13
+
14
+ class TestAnnotation:
15
+ def test_create_delete(self, document):
16
+ assert len(document.notes.list().results) == 1
17
+ note = document.notes.create(
18
+ "Test Note", 0, "<p>Note content!</p>", x1=0.1, y1=0.1, x2=0.2, y2=0.2
19
+ )
20
+ assert len(document.notes.list().results) == 2
21
+ for note in document.notes:
22
+ assert isinstance(note, Annotation)
23
+ note.delete()
24
+ assert len(document.notes.list().results) == 1
25
+
26
+ def test_create_page_note(self, document):
27
+ note = document.notes.create("Test Note", 0, "<p>Page note!</p>")
28
+ assert note
29
+ note.delete()
30
+
31
+ def test_create_partial_coords(self, document):
32
+ with pytest.raises(ValueError):
33
+ document.notes.create("Test Note", 0, "<p>Page note!</p>", x1=0.5)
34
+
35
+ def test_create_invalid_coords(self, document):
36
+ with pytest.raises(ValueError):
37
+ document.notes.create(
38
+ "Test Note", 0, "<p>Page note!</p>", x1=0.5, y1=1.5, x2=2, y2=3
39
+ )
40
+
41
+ def test_str(self, document):
42
+ assert str(document.notes[0])
43
+
44
+ def test_alias(self, document):
45
+ assert document.notes is document.annotations
46
+
47
+ def test_location(self, document):
48
+ note = document.notes[0]
49
+ assert note.location.top < note.location.bottom
50
+ assert note.location.left < note.location.right
@@ -0,0 +1,114 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+
7
+ # Third Party
8
+ import pytest
9
+
10
+ # DocumentCloud
11
+ from documentcloud.documents import Document
12
+ from documentcloud.exceptions import DuplicateObjectError
13
+
14
+
15
+ class TestAPIResults:
16
+ def test_str(self, client):
17
+ results = client.documents.list()
18
+ assert str(results)
19
+
20
+ def test_getitem(self, client):
21
+ results = client.documents.list()
22
+ assert isinstance(results[0], Document)
23
+
24
+ def test_getitem_paginate(self, client):
25
+ results = client.documents.list(per_page=2)
26
+ assert isinstance(results[3], Document)
27
+
28
+ def test_getitem_index_error(self, client):
29
+ # pylint: disable=pointless-statement
30
+ results = client.documents.list()
31
+ index = len(list(results)) + 1
32
+ with pytest.raises(IndexError):
33
+ results[index]
34
+
35
+ def test_len(self, client):
36
+ results = client.documents.list()
37
+ assert len(results.results) > 0
38
+
39
+ def test_iter(self, client):
40
+ results = client.documents.list()
41
+ for doc in results:
42
+ assert isinstance(doc, Document)
43
+
44
+ def test_next(self, client):
45
+ results = client.documents.list(user=client.user_id, per_page=1)
46
+ assert len(results.results) > 0
47
+ while results.next is not None:
48
+ results = results.next
49
+
50
+ def test_previous(self, client):
51
+ results = client.documents.list(user=client.user_id, per_page=1).next
52
+ assert results.previous
53
+ assert results.previous.previous is None
54
+
55
+
56
+ class TestAPISet:
57
+ def test_init(self, project_factory, document):
58
+ project = project_factory()
59
+ document_list = project.document_list
60
+ project.document_list = [document]
61
+ project.document_list = document_list
62
+
63
+ def test_init_bad_types(self, project):
64
+ with pytest.raises(TypeError):
65
+ project.document_list = [1, 2, 3]
66
+
67
+ def test_init_dupes(self, project, document):
68
+ with pytest.raises(DuplicateObjectError):
69
+ project.document_list = [document, document]
70
+
71
+ def test_append(self, project, document_factory):
72
+ document = document_factory()
73
+ project.document_list.append(document)
74
+ assert project.document_list[-1] == document
75
+ project.document_list.remove(document)
76
+
77
+ def test_append_bad_type(self, project):
78
+ with pytest.raises(TypeError):
79
+ project.document_list.append(1)
80
+
81
+ def test_append_dupes(self, project, document):
82
+ with pytest.raises(DuplicateObjectError):
83
+ project.document_list.append(document)
84
+
85
+ def test_add(self, project, document_factory):
86
+ document = document_factory()
87
+ project.document_list.add(document)
88
+ assert document in project.document_list
89
+ project.document_list.remove(document)
90
+
91
+ def test_add_bad_type(self, project):
92
+ with pytest.raises(TypeError):
93
+ project.document_list.add(1)
94
+
95
+ def test_add_dupe(self, project, document):
96
+ assert document in project.document_list
97
+ length = len(project.document_list)
98
+ project.document_list.add(document)
99
+ assert document in project.document_list
100
+ assert len(project.document_list) == length
101
+
102
+ def test_extend(self, project, document_factory):
103
+ document = document_factory()
104
+ project.document_list.extend([document])
105
+ assert document == project.document_list[-1]
106
+ project.document_list.remove(document)
107
+
108
+ def test_extend_bad_type(self, project):
109
+ with pytest.raises(TypeError):
110
+ project.document_list.extend([1])
111
+
112
+ def test_extend_dupe(self, project, document):
113
+ with pytest.raises(DuplicateObjectError):
114
+ project.document_list.extend([document])
@@ -0,0 +1,113 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ import time
6
+
7
+ # Third Party
8
+ import pytest
9
+ import ratelimit
10
+
11
+ # DocumentCloud
12
+ from documentcloud.constants import RATE_LIMIT
13
+ from documentcloud.exceptions import APIError, CredentialsFailedError
14
+
15
+ # pylint: disable=protected-access
16
+
17
+
18
+ def test_set_tokens_credentials(client):
19
+ """Test setting the tokens using credentials"""
20
+ client.refresh_token = None
21
+ del client.session.headers["Authorization"]
22
+ client._set_tokens()
23
+ assert client.refresh_token
24
+ assert "Authorization" in client.session.headers
25
+
26
+
27
+ def test_set_tokens_refresh(client):
28
+ """Test setting the tokens using refresh token"""
29
+ # first set tokens sets, refresh token, second one uses it
30
+ client.refresh_token = None
31
+ del client.session.headers["Authorization"]
32
+ client._set_tokens()
33
+ client._set_tokens()
34
+ assert client.refresh_token
35
+ assert "Authorization" in client.session.headers
36
+
37
+
38
+ def test_set_tokens_none(public_client):
39
+ """Test setting the tokens with no credentials"""
40
+ public_client._set_tokens()
41
+ assert public_client.refresh_token is None
42
+ assert "Authorization" not in public_client.session.headers
43
+
44
+
45
+ def test_get_tokens(client):
46
+ """Test getting access and refresh tokens using valid credentials"""
47
+ access, refresh = client._get_tokens(client.username, client.password)
48
+ assert access
49
+ assert refresh
50
+
51
+
52
+ def test_get_tokens_bad_credentials(client):
53
+ """Test getting access and refresh tokens using invalid credentials"""
54
+ with pytest.raises(CredentialsFailedError):
55
+ client._get_tokens(client.username, "foo")
56
+
57
+
58
+ def test_refresh_tokens(client):
59
+ """Test refreshing the tokens"""
60
+ access, refresh = client._refresh_tokens(client.refresh_token)
61
+ assert access
62
+ assert refresh
63
+
64
+
65
+ def test_user_id(client):
66
+ assert client.user_id
67
+
68
+
69
+ def test_user_id_public(public_client):
70
+ # pylint: disable=pointless-statement
71
+ with pytest.raises(APIError, match=r"404"):
72
+ public_client.user_id
73
+
74
+
75
+ def test_bad_attr(client):
76
+ with pytest.raises(AttributeError):
77
+ assert client.foo
78
+
79
+
80
+ def test_rate_limit(rate_client):
81
+ with pytest.raises(ratelimit.RateLimitException):
82
+ for _ in range(RATE_LIMIT * 2):
83
+ rate_client.users.get("me")
84
+
85
+
86
+ @pytest.mark.short
87
+ @pytest.mark.vcr(cassette_library_dir="tests/cassettes/short_fixtures")
88
+ def test_expired_access_token(short_client, record_mode):
89
+ # get fresh tokens
90
+ short_client._set_tokens()
91
+ old_refresh_token = short_client.refresh_token
92
+ # wait for the access token to expire
93
+ if record_mode == "all":
94
+ time.sleep(3)
95
+ # make a request
96
+ assert short_client.users.get("me")
97
+ # check the refresh token was updated
98
+ assert old_refresh_token != short_client.refresh_token
99
+
100
+
101
+ @pytest.mark.short
102
+ @pytest.mark.vcr(cassette_library_dir="tests/cassettes/short_fixtures")
103
+ def test_expired_refresh_token(short_client, record_mode):
104
+ # get fresh tokens
105
+ short_client._set_tokens()
106
+ old_refresh_token = short_client.refresh_token
107
+ # wait for the access and refresh tokens to expire
108
+ if record_mode == "all":
109
+ time.sleep(6)
110
+ # make a request
111
+ assert short_client.users.get("me")
112
+ # check the refresh token was updated
113
+ assert old_refresh_token != short_client.refresh_token
@@ -0,0 +1,238 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+ from datetime import datetime
7
+
8
+ # Third Party
9
+ import pytest
10
+
11
+ # DocumentCloud
12
+ from documentcloud.documents import Mention
13
+ from documentcloud.exceptions import APIError, DoesNotExistError
14
+ from documentcloud.organizations import Organization
15
+ from documentcloud.users import User
16
+
17
+ # pylint: disable=protected-access
18
+
19
+
20
+ class TestDocument:
21
+ def test_str(self, document):
22
+ assert str(document) == document.title
23
+
24
+ def test_dates(self, document):
25
+ for date_field in document.date_fields:
26
+ assert isinstance(getattr(document, date_field), datetime)
27
+
28
+ @pytest.mark.parametrize(
29
+ "attr",
30
+ [
31
+ "full_text_url",
32
+ "full_text",
33
+ "thumbnail_image_url",
34
+ "small_image",
35
+ "normal_image_url_list",
36
+ "large_image_url",
37
+ "page_text",
38
+ "json_text_url",
39
+ "pdf",
40
+ ],
41
+ )
42
+ def test_getattr(self, document, attr):
43
+ assert getattr(document, attr)
44
+
45
+ @pytest.mark.parametrize(
46
+ "attr",
47
+ [
48
+ "get_full_text_url",
49
+ "get_full_text",
50
+ "get_thumbnail_image_url",
51
+ "get_small_image",
52
+ "get_normal_image_url_list",
53
+ "get_large_image_url",
54
+ "get_page_text",
55
+ "get_json_text_url",
56
+ "get_pdf",
57
+ ],
58
+ )
59
+ def test_getattr_method(self, document, attr):
60
+ assert getattr(document, attr)()
61
+
62
+ @pytest.mark.parametrize(
63
+ "attr",
64
+ [
65
+ "full_text_url",
66
+ "get_full_text",
67
+ "thumbnail_image_url",
68
+ "get_small_image",
69
+ "normal_image_url_list",
70
+ "get_large_image_url",
71
+ ],
72
+ )
73
+ def test_dir(self, document, attr):
74
+ assert attr in dir(document)
75
+
76
+ def test_mentions(self, client, document):
77
+ document = client.documents.search(
78
+ f"document:{document.id} text", mentions="true"
79
+ )[0]
80
+ assert document.mentions
81
+ mention = document.mentions[0]
82
+ assert mention.page
83
+ assert "<em>text</em>" in mention.text
84
+
85
+ def test_mentions_nosearch(self, document):
86
+ assert not document.mentions
87
+
88
+ def test_user(self, document):
89
+ assert document._user is None
90
+ assert isinstance(document.user, User)
91
+ assert document.user == document._user
92
+
93
+ def test_user_expanded(self, client, document):
94
+ document = client.documents.get(document.id, expand=["user"])
95
+ assert document._user is not None
96
+ assert document._user == document.user
97
+
98
+ def test_organization(self, document):
99
+ assert document._organization is None
100
+ assert isinstance(document.organization, Organization)
101
+ assert document.organization == document._organization
102
+
103
+ @pytest.mark.parametrize(
104
+ "attr",
105
+ [
106
+ "id",
107
+ "access",
108
+ "asset_url",
109
+ "canonical_url",
110
+ "created_at",
111
+ "data",
112
+ "description",
113
+ "edit_access",
114
+ "language",
115
+ "organization_id",
116
+ "page_count",
117
+ "page_spec",
118
+ "projects",
119
+ "related_article",
120
+ "published_url",
121
+ "slug",
122
+ "source",
123
+ "status",
124
+ "title",
125
+ "updated_at",
126
+ "user_id",
127
+ "pages",
128
+ "contributor",
129
+ "contributor_organization",
130
+ "contributor_organization_slug",
131
+ ],
132
+ )
133
+ def test_attrs(self, document, attr):
134
+ assert getattr(document, attr)
135
+
136
+ def test_save(self, client, document):
137
+ assert document.source == "DocumentCloud"
138
+ document.source = "MuckRock"
139
+ document.save()
140
+ document = client.documents.get(document.id)
141
+ assert document.source == "MuckRock"
142
+
143
+ def test_delete(self, client, document_factory):
144
+ document = document_factory()
145
+ document.delete()
146
+
147
+ with pytest.raises(DoesNotExistError):
148
+ client.documents.get(document.id)
149
+
150
+ def test_section(self, document_factory):
151
+ document = document_factory()
152
+ assert len(document.sections.list().results) == 0
153
+ section = document.sections.create("Test Section", 0)
154
+ assert str(section) == "Test Section - p0"
155
+ assert section.page == 0
156
+ assert section == document.sections.list()[0]
157
+
158
+
159
+ class TestDocumentClient:
160
+ def test_search(self, client, document):
161
+ documents = client.documents.search(
162
+ f"document:{document.id} simple"
163
+ )
164
+ assert documents
165
+
166
+ def test_list(self, client):
167
+ # list and all are aliases
168
+ all_documents = client.documents.all()
169
+ my_documents = client.documents.list(user=client.user_id)
170
+ assert len(list(all_documents)) > len(list(my_documents.results))
171
+
172
+ def test_upload_url(self, document_factory):
173
+ document = document_factory()
174
+ assert document.status == "success"
175
+
176
+ def test_public_upload(self, public_client):
177
+ with pytest.raises(APIError, match=r"403"):
178
+ public_client.documents.upload("tests/test.pdf")
179
+
180
+ def test_upload_file(self, document_factory):
181
+ with open("tests/test.pdf", "rb") as pdf:
182
+ document = document_factory(pdf)
183
+ assert document.status == "success"
184
+
185
+
186
+ def test_upload_file_path(self, document_factory):
187
+ document = document_factory("tests/test.pdf")
188
+ assert document.status == "success"
189
+
190
+ def test_upload_big_file(self, client, mocker):
191
+ mocker.patch("os.path.getsize", return_value=502 * 1024 * 1024)
192
+ with pytest.raises(ValueError):
193
+ client.documents.upload("tests/test.pdf")
194
+
195
+ def test_upload_dir(self, client):
196
+ documents = client.documents.upload_directory("tests/pdfs/")
197
+ assert len(documents) == 2
198
+
199
+ def test_format_upload_parameters(self, client):
200
+ with pytest.warns(UserWarning):
201
+ params = client.documents._format_upload_parameters(
202
+ "tests/test.pdf", access="private", secure=True, project=2, foo="bar"
203
+ )
204
+ assert params == {"title": "test", "access": "private", "projects": [2]}
205
+
206
+ def test_delete(self, document_factory, client):
207
+ document = document_factory()
208
+ client.documents.delete(document.id)
209
+
210
+ with pytest.raises(DoesNotExistError):
211
+ client.documents.get(document.id)
212
+
213
+
214
+ class TestMention:
215
+ def test_mention(self):
216
+ mention = Mention("page_no_42", "text")
217
+ assert str(mention) == '42 - "text"'
218
+
219
+
220
+ class TestSection:
221
+ def test_create_delete(self, document_factory):
222
+ document = document_factory()
223
+ assert len(document.sections.list().results) == 0
224
+ section = document.sections.create("Test Section", 0)
225
+ assert len(document.sections.list().results) == 1
226
+
227
+ # may not have two sections on the same page
228
+ with pytest.raises(APIError):
229
+ document.sections.create("Test Section 2", 0)
230
+
231
+ section.delete()
232
+ assert len(document.sections.list().results) == 0
233
+
234
+ def test_str(self, document):
235
+ assert str(document.sections[0])
236
+
237
+ def test_page(self, document):
238
+ assert document.sections[0].page == 0
@@ -0,0 +1,11 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+
7
+
8
+ def test_organization(client):
9
+ user = client.users.get(client.user_id)
10
+ organization = client.organizations.get(user.organization)
11
+ assert str(organization) == organization.name
@@ -0,0 +1,111 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+
7
+ # Third Party
8
+ import pytest
9
+
10
+ # DocumentCloud
11
+ from documentcloud.documents import Document
12
+ from documentcloud.exceptions import DoesNotExistError, MultipleObjectsReturnedError
13
+
14
+
15
+ class TestProject:
16
+ def test_str(self, project):
17
+ assert str(project) == project.title
18
+
19
+ def test_save(self, client, project, document_factory):
20
+ document = document_factory()
21
+ assert document not in project.documents
22
+ project.documents.append(document)
23
+ # put is an alias for save
24
+ project.put()
25
+ project = client.projects.get(project.id)
26
+ assert document in project.documents
27
+
28
+ def test_document_list(self, project):
29
+ assert len(project.document_list) > 0
30
+ assert all(isinstance(d, Document) for d in project.document_list)
31
+
32
+ def test_document_list_paginate(self, project):
33
+ # pylint: disable=protected-access
34
+ length = len(project.document_list)
35
+ assert length > 1
36
+ # clear cache
37
+ project._document_list = None
38
+ # set per page to 1 to force pagination
39
+ project._per_page = 1
40
+ assert len(project.document_list) == length
41
+
42
+ def test_document_list_setter(self, project, document):
43
+ assert document in project.document_list
44
+ # setting to none clears it and sets to an empty list
45
+ project.document_list = None
46
+ assert document not in project.document_list
47
+ # documents is an alias for document_list
48
+ project.documents = [document]
49
+ assert document in project.document_list
50
+ with pytest.raises(TypeError):
51
+ project.document_list = document
52
+
53
+ def test_document_ids(self, project, document):
54
+ assert document.id in project.document_ids
55
+
56
+ def test_get_document(self, project, document):
57
+ assert project.get_document(document.id)
58
+
59
+ def test_get_document_missing(self, project, document_factory):
60
+ document = document_factory()
61
+ with pytest.raises(DoesNotExistError):
62
+ project.get_document(document.id)
63
+
64
+
65
+ class TestProjectClient:
66
+ def test_list(self, client):
67
+ all_projects = client.projects.list()
68
+ my_projects = client.projects.all()
69
+ # assert len(all_projects.results) > len(my_projects.results)
70
+ assert len(client.projects.list(user=client.user_id).results) == len(
71
+ my_projects.results
72
+ )
73
+
74
+ def test_get_id(self, client, project):
75
+ assert client.projects.get(id=project.id)
76
+
77
+ def test_get_title(self, client, project):
78
+ assert client.projects.get(title=project.title)
79
+
80
+ def test_get_nothing(self, client):
81
+ with pytest.raises(ValueError):
82
+ client.projects.get()
83
+
84
+ def test_get_both(self, client, project):
85
+ with pytest.raises(ValueError):
86
+ client.projects.get(id=project.id, title=project.title)
87
+
88
+ def test_get_by_id(self, client, project):
89
+ assert client.projects.get_by_id(project.id)
90
+
91
+ def test_get_by_title(self, client, project):
92
+ assert client.projects.get_by_title(project.title)
93
+
94
+ def test_get_by_title_multiple(self, client, project_factory):
95
+ for _ in range(2):
96
+ project_factory(title="Dupe")
97
+ with pytest.raises(MultipleObjectsReturnedError):
98
+ client.projects.get_by_title("Dupe")
99
+
100
+ def test_get_or_create_by_title_get(self, client, project):
101
+ title = project.title
102
+ project, created = client.projects.get_or_create_by_title(title)
103
+ assert project.title == title
104
+ assert not created
105
+
106
+ def test_get_or_create_by_title_create(self, client):
107
+ title = "Created Title"
108
+ project, created = client.projects.get_or_create_by_title(title)
109
+ assert project.title == title
110
+ project.delete()
111
+ assert created
@@ -0,0 +1,32 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+
7
+ # Third Party
8
+ import pytest
9
+
10
+ # DocumentCloud
11
+ from documentcloud.exceptions import APIError
12
+
13
+
14
+ class TestSection:
15
+ def test_create_delete(self, document_factory):
16
+ document = document_factory()
17
+ assert len(document.sections.list().results) == 0
18
+ section = document.sections.create("Test Section", 0)
19
+ assert len(document.sections.list().results) == 1
20
+
21
+ # may not have two sections on the same page
22
+ with pytest.raises(APIError):
23
+ document.sections.create("Test Section 2", 0)
24
+
25
+ section.delete()
26
+ assert len(document.sections.list().results) == 0
27
+
28
+ def test_str(self, document):
29
+ assert str(document.sections[0])
30
+
31
+ def test_page(self, document):
32
+ assert document.sections[0].page == 0
@@ -0,0 +1,25 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # DocumentCloud
5
+ from documentcloud.toolbox import get_id
6
+
7
+
8
+ def test_get_id_number():
9
+ assert get_id(42) == 42
10
+
11
+
12
+ def test_get_id_str():
13
+ assert get_id("42") == "42"
14
+
15
+
16
+ def test_get_id_prefix():
17
+ assert get_id("42-foo-bar") == "42"
18
+
19
+
20
+ def test_get_id_postfix():
21
+ assert get_id("foo-bar-42") == "42"
22
+
23
+
24
+ def test_get_id_both():
25
+ assert get_id("42-foo-bar-123") == "42"
@@ -0,0 +1,10 @@
1
+ # Future
2
+ from __future__ import division, print_function, unicode_literals
3
+
4
+ # Standard Library
5
+ from builtins import str
6
+
7
+
8
+ def test_user(client):
9
+ user = client.users.get(client.user_id)
10
+ assert str(user) == user.username
@@ -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"""