pyzotero 1.6.6__py3-none-any.whl → 1.6.8__py3-none-any.whl

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.
_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.6.6'
16
- __version_tuple__ = version_tuple = (1, 6, 6)
15
+ __version__ = version = '1.6.8'
16
+ __version_tuple__ = version_tuple = (1, 6, 8)
@@ -0,0 +1,151 @@
1
+ # This is a modified version of httpx_file:
2
+ # The aiofiles dependency has been removed by modifying the async functionality to use
3
+ # asyncio instead. A specific test for this modification can be found in tests/test_async.py
4
+ # https://github.com/nuno-andre/httpx-file
5
+
6
+
7
+ # The license and copyright notice are reproduced below
8
+ # Copyright (c) 2021, Nuno André Novo
9
+ # All rights reserved.
10
+
11
+ # Redistribution and use in source and binary forms, with or without
12
+ # modification, are permitted provided that the following conditions are met:
13
+ # * Redistributions of source code must retain the above copyright notice, this
14
+ # list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the <copyright holder> nor the names of its contributors
19
+ # may be used to endorse or promote products derived from this software without
20
+ # specific prior written permission.
21
+
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
23
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
26
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
29
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
30
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
31
+ # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+
33
+ import asyncio
34
+ from pathlib import Path
35
+ from typing import Optional, Tuple
36
+
37
+ import httpx
38
+ from httpx import (
39
+ AsyncBaseTransport,
40
+ BaseTransport,
41
+ ByteStream,
42
+ Request,
43
+ Response,
44
+ )
45
+ from httpx import (
46
+ AsyncClient as _AsyncClient,
47
+ )
48
+ from httpx import (
49
+ Client as _Client,
50
+ )
51
+ from httpx._utils import URLPattern
52
+
53
+
54
+ # monkey patch to fix httpx URL parsing
55
+ def is_relative_url(self):
56
+ return not (self._uri_reference.scheme or self._uri_reference.host)
57
+
58
+
59
+ def is_absolute_url(self):
60
+ return not self.is_relative_url
61
+
62
+
63
+ httpx.URL.is_relative_url = property(is_relative_url) # type: ignore
64
+ httpx.URL.is_absolute_url = property(is_absolute_url) # type: ignore
65
+
66
+
67
+ class FileTransport(AsyncBaseTransport, BaseTransport):
68
+ def _handle(self, request: Request) -> Tuple[Optional[int], httpx.Headers]:
69
+ if request.url.host and request.url.host != "localhost":
70
+ raise NotImplementedError("Only local paths are allowed")
71
+ if request.method in {"PUT", "DELETE"}:
72
+ status = 501 # Not Implemented
73
+ elif request.method not in {"GET", "HEAD"}:
74
+ status = 405 # Method Not Allowed
75
+ else:
76
+ status = None
77
+ return status, request.headers
78
+
79
+ def handle_request(self, request: Request) -> Response:
80
+ status, headers = self._handle(request)
81
+ stream = None
82
+ if not status:
83
+ parts = request.url.path.split("/")
84
+ if parts[1].endswith((":", "|")):
85
+ parts[1] = parts[1][:-1] + ":"
86
+ parts.pop(0)
87
+ ospath = Path("/".join(parts))
88
+ try:
89
+ content = ospath.read_bytes()
90
+ status = 200
91
+ except FileNotFoundError:
92
+ status = 404
93
+ except PermissionError:
94
+ status = 403
95
+ else:
96
+ stream = ByteStream(content)
97
+ headers["Content-Length"] = str(len(content))
98
+ return Response(
99
+ status_code=status,
100
+ headers=headers,
101
+ stream=stream,
102
+ extensions=dict(),
103
+ )
104
+
105
+ async def handle_async_request(self, request: Request) -> Response:
106
+ status, headers = self._handle(request)
107
+ stream = None
108
+ if not status:
109
+ parts = request.url.path.split("/")
110
+ if parts[1].endswith((":", "|")):
111
+ parts[1] = parts[1][:-1] + ":"
112
+ parts.pop(0)
113
+ ospath = Path("/".join(parts))
114
+ try:
115
+ loop = asyncio.get_event_loop()
116
+ content = await loop.run_in_executor(None, ospath.read_bytes)
117
+ status = 200
118
+ except FileNotFoundError:
119
+ status = 404
120
+ except PermissionError:
121
+ status = 403
122
+ else:
123
+ stream = ByteStream(content)
124
+ headers["Content-Length"] = str(len(content))
125
+ return Response(
126
+ status_code=status,
127
+ headers=headers,
128
+ stream=stream,
129
+ extensions=dict(),
130
+ )
131
+
132
+
133
+ class Client(_Client):
134
+ def __init__(self, **kwargs) -> None:
135
+ super().__init__(**kwargs)
136
+ self.mount("file://", FileTransport())
137
+
138
+ def mount(self, protocol: str, transport: BaseTransport) -> None:
139
+ self._mounts.update({URLPattern(protocol): transport})
140
+
141
+
142
+ class AsyncClient(_AsyncClient):
143
+ def __init__(self, **kwargs) -> None:
144
+ super().__init__(**kwargs)
145
+ self.mount("file://", FileTransport())
146
+
147
+ def mount(self, protocol: str, transport: AsyncBaseTransport) -> None:
148
+ self._mounts.update({URLPattern(protocol): transport})
149
+
150
+
151
+ __all__ = ["FileTransport", "AsyncClient", "Client"]
pyzotero/zotero.py CHANGED
@@ -44,6 +44,7 @@ from httpx import Request
44
44
  import pyzotero as pz
45
45
 
46
46
  from . import zotero_errors as ze
47
+ from .filetransport import Client as File_Client
47
48
 
48
49
  # Avoid hanging the application if there's no server response
49
50
  timeout = 30
@@ -190,38 +191,9 @@ def retrieve(func):
190
191
  and self.content.search(str(self.request.url)).group(0)
191
192
  or "bib"
192
193
  )
193
- # JSON by default
194
- formats = {
195
- "application/atom+xml": "atom",
196
- "application/x-bibtex": "bibtex",
197
- "application/json": "json",
198
- "text/html": "snapshot",
199
- "text/plain": "plain",
200
- "application/pdf; charset=utf-8": "pdf",
201
- "application/pdf": "pdf",
202
- "application/msword": "doc",
203
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
204
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
205
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
206
- "application/zip": "zip",
207
- "application/epub+zip": "zip",
208
- "audio/mpeg": "mp3",
209
- "video/mp4": "mp4",
210
- "audio/x-wav": "wav",
211
- "video/x-msvideo": "avi",
212
- "application/octet-stream": "octet",
213
- "application/x-tex": "tex",
214
- "application/x-texinfo": "texinfo",
215
- "image/jpeg": "jpeg",
216
- "image/png": "png",
217
- "image/gif": "gif",
218
- "image/tiff": "tiff",
219
- "application/postscript": "postscript",
220
- "application/rtf": "rtf",
221
- }
222
194
  # select format, or assume JSON
223
195
  content_type_header = self.request.headers["Content-Type"].lower() + ";"
224
- fmt = formats.get(
196
+ fmt = self.formats.get(
225
197
  # strip "; charset=..." segment
226
198
  content_type_header[0 : content_type_header.index(";")],
227
199
  "json",
@@ -321,12 +293,44 @@ class Zotero:
321
293
  self.tag_data = False
322
294
  self.request = None
323
295
  self.snapshot = False
324
- self.client = httpx.Client(headers=self.default_headers())
296
+ self.client = httpx.Client(
297
+ headers=self.default_headers(), follow_redirects=True
298
+ )
325
299
  # these aren't valid item fields, so never send them to the server
326
300
  self.temp_keys = set(["key", "etag", "group_id", "updated"])
327
301
  # determine which processor to use for the parsed content
328
302
  self.fmt = re.compile(r"(?<=format=)\w+")
329
303
  self.content = re.compile(r"(?<=content=)\w+")
304
+ # JSON by default
305
+ self.formats = {
306
+ "application/atom+xml": "atom",
307
+ "application/x-bibtex": "bibtex",
308
+ "application/json": "json",
309
+ "text/html": "snapshot",
310
+ "text/plain": "plain",
311
+ "text/markdown": "plain",
312
+ "application/pdf; charset=utf-8": "pdf",
313
+ "application/pdf": "pdf",
314
+ "application/msword": "doc",
315
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
316
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
317
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
318
+ "application/zip": "zip",
319
+ "application/epub+zip": "zip",
320
+ "audio/mpeg": "mp3",
321
+ "video/mp4": "mp4",
322
+ "audio/x-wav": "wav",
323
+ "video/x-msvideo": "avi",
324
+ "application/octet-stream": "octet",
325
+ "application/x-tex": "tex",
326
+ "application/x-texinfo": "texinfo",
327
+ "image/jpeg": "jpeg",
328
+ "image/png": "png",
329
+ "image/gif": "gif",
330
+ "image/tiff": "tiff",
331
+ "application/postscript": "postscript",
332
+ "application/rtf": "rtf",
333
+ }
330
334
  self.processors = {
331
335
  "bib": self._bib_processor,
332
336
  "citation": self._citation_processor,
@@ -479,13 +483,28 @@ class Zotero:
479
483
  # Unfortunately, httpx doesn't like to merge query paramaters in the url string and passed params
480
484
  # so we strip the url params, combining them with our existing url_params
481
485
  final_url, final_params = merge_params(full_url, merged_params)
482
- self.request = self.client.get(
483
- url=final_url,
484
- params=final_params,
485
- headers=self.default_headers(),
486
- timeout=timeout,
487
- )
488
- self.request.encoding = "utf-8"
486
+ # file URI errors are raised immediately so we have to try here
487
+ try:
488
+ self.request = self.client.get(
489
+ url=final_url,
490
+ params=final_params,
491
+ headers=self.default_headers(),
492
+ timeout=timeout,
493
+ )
494
+ self.request.encoding = "utf-8"
495
+ except httpx.UnsupportedProtocol:
496
+ # File URI handler logic
497
+ fc = File_Client()
498
+ request = fc.get(
499
+ url=final_url,
500
+ params=final_params,
501
+ headers=self.default_headers(),
502
+ timeout=timeout,
503
+ follow_redirects=True,
504
+ )
505
+ self.request = request
506
+ # since we'll be writing bytes, we need to set this to a type that will trigger the bytes processor
507
+ self.request.headers["Content-Type"] = "text/plain"
489
508
  try:
490
509
  self.request.raise_for_status()
491
510
  except httpx.HTTPError as exc:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pyzotero
3
- Version: 1.6.6
3
+ Version: 1.6.8
4
4
  Summary: Python wrapper for the Zotero API
5
5
  Author-email: Stephan Hügel <urschrei@gmail.com>
6
6
  License: # Blue Oak Model License
@@ -68,6 +68,7 @@ Requires-Dist: pytest>=7.4.2; extra == "test"
68
68
  Requires-Dist: httpretty; extra == "test"
69
69
  Requires-Dist: python-dateutil; extra == "test"
70
70
  Requires-Dist: ipython; extra == "test"
71
+ Requires-Dist: pytest-asyncio; extra == "test"
71
72
 
72
73
  [![Supported Python versions](https://img.shields.io/pypi/pyversions/Pyzotero.svg?style=flat)](https://pypi.python.org/pypi/Pyzotero/) [![Docs](https://readthedocs.org/projects/pyzotero/badge/?version=latest)](http://pyzotero.readthedocs.org/en/latest/?badge=latest) [![PyPI Version](https://img.shields.io/pypi/v/Pyzotero.svg)](https://pypi.python.org/pypi/Pyzotero) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyzotero/badges/version.svg)](https://anaconda.org/conda-forge/pyzotero) [![Downloads](https://pepy.tech/badge/pyzotero)](https://pepy.tech/project/pyzotero)
73
74
 
@@ -0,0 +1,11 @@
1
+ _version.py,sha256=cmsiv9CCkKr77S4CsQCya3nhc7DDRNXIH_h8pHWpeYE,411
2
+ pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
3
+ pyzotero/filetransport.py,sha256=FSdKYYX8ITFhSzqagX8U3y-SalH1aThtedDje42v3oA,5537
4
+ pyzotero/zotero.py,sha256=8oxenEitZ-Btca5jy82y39rJcNYmnY6cxiKY2uU4MwM,77347
5
+ pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
6
+ pyzotero-1.6.8.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
7
+ pyzotero-1.6.8.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
8
+ pyzotero-1.6.8.dist-info/METADATA,sha256=js6NjlnRvg6OzTSa1UNAv6yCD9V4sBClAxhz8v1ao_8,7337
9
+ pyzotero-1.6.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10
+ pyzotero-1.6.8.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
11
+ pyzotero-1.6.8.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- _version.py,sha256=Nz1l8vGPOAOAlxWDepNu28nfH9fOKk0GnGSnVyZJsFE,411
2
- pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
3
- pyzotero/zotero.py,sha256=9mwSEa3Z97Bdni9p_yMqeKBEYQFUvRdwFJAIaf0gg5A,76539
4
- pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
5
- pyzotero-1.6.6.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
6
- pyzotero-1.6.6.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
7
- pyzotero-1.6.6.dist-info/METADATA,sha256=fw5aXyuKKiBOUD74CbxLzmDDZcqw-E9XBV-HNecB0W8,7290
8
- pyzotero-1.6.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
9
- pyzotero-1.6.6.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
10
- pyzotero-1.6.6.dist-info/RECORD,,