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.
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/PKG-INFO +21 -5
- python_documentcloud-4.3.0/documentcloud/client.py +62 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/constants.py +42 -1
- python_documentcloud-4.3.0/documentcloud/exceptions.py +12 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/PKG-INFO +21 -5
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/SOURCES.txt +10 -1
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/requires.txt +3 -2
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/setup.py +2 -1
- python_documentcloud-4.3.0/tests/test_annotations.py +50 -0
- python_documentcloud-4.3.0/tests/test_base.py +114 -0
- python_documentcloud-4.3.0/tests/test_client.py +113 -0
- python_documentcloud-4.3.0/tests/test_documents.py +238 -0
- python_documentcloud-4.3.0/tests/test_organizations.py +11 -0
- python_documentcloud-4.3.0/tests/test_projects.py +111 -0
- python_documentcloud-4.3.0/tests/test_sections.py +32 -0
- python_documentcloud-4.3.0/tests/test_toolbox.py +25 -0
- python_documentcloud-4.3.0/tests/test_users.py +10 -0
- python-documentcloud-4.1.3/documentcloud/client.py +0 -177
- python-documentcloud-4.1.3/documentcloud/exceptions.py +0 -39
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/LICENSE +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/README.md +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/__init__.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/addon.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/annotations.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/base.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/documents.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/organizations.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/projects.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/sections.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/toolbox.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/documentcloud/users.py +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/dependency_links.txt +0 -0
- {python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
".
|
|
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
|
{python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/PKG-INFO
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-documentcloud
|
|
3
|
-
Version: 4.
|
|
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
|
-
|
|
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
|
-
|
{python-documentcloud-4.1.3 → python_documentcloud-4.3.0}/python_documentcloud.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
@@ -7,7 +7,7 @@ with open("README.md", "r") as fh:
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name="python-documentcloud",
|
|
10
|
-
version="4.
|
|
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"
|
|
@@ -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"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|