Pyzotero: a Python client for the Zotero API pyzotero.readthedocs.io
zotero

Replace httpretty with httpx MockTransport


Signed-off-by: Stephan Hügel <shugel@tcd.ie>

+511 -323
+4 -2
pyproject.toml
··· 43 43 dev = [ 44 44 "pytest >= 8.4.2", 45 45 "pytz>=2025.2", 46 - "httpretty >= 1.1.4", 47 46 "python-dateutil", 48 47 "ipython", 49 48 "pytest-asyncio", 50 49 "pytest-cov>=6.0.0", 51 - "tzdata>=2025.2" 50 + "tzdata>=2025.2", 52 51 ] 53 52 doc = [ 54 53 "sphinx", ··· 109 108 ignore = ["ANN001", "ANN003", "ANN202", "ANN201", "DOC201", "E501", "PLR0904", "PLR0913", "PLR0917", "SLF001", "FIX002", "D400", "D415"] 110 109 fixable = ["ALL"] 111 110 unfixable = [] 111 + 112 + [tool.ruff.lint.per-file-ignores] 113 + "tests/*" = ["S101"] # Allow assert in tests 112 114 113 115 [tool.ruff.format] 114 116 # Like Black, use double quotes for strings.
+3 -3
src/pyzotero/_client.py
··· 63 63 preserve_json_order=False, 64 64 locale="en-US", 65 65 local=False, 66 + client=None, 66 67 ): 67 68 self.client = None 68 69 """Store Zotero credentials""" ··· 95 96 self.tag_data = False 96 97 self.request = None 97 98 self.snapshot = False 98 - self.client = httpx.Client( 99 + self.client = client or httpx.Client( 99 100 headers=self.default_headers(), 100 101 follow_redirects=True, 101 102 ) ··· 927 928 build_url(self.endpoint, query_string), 928 929 params=params, 929 930 ) 930 - with httpx.Client() as client: 931 - response = client.send(r) 931 + response = self.client.send(r) 932 932 # now split up the URL 933 933 result = urlparse(str(response.url)) 934 934 # construct cache key
+1 -2
src/pyzotero/_decorators.py
··· 50 50 build_url(self.endpoint, query_string), 51 51 params=params, 52 52 ) 53 - with httpx.Client() as client: 54 - response = client.send(r) 53 + response = self.client.send(r) 55 54 56 55 # now split up the URL 57 56 result = urlparse(str(response.url))
+185
tests/mock_client.py
··· 1 + """Mock HTTP client for testing using httpx MockTransport.""" 2 + 3 + from __future__ import annotations 4 + 5 + import json 6 + from collections.abc import Callable 7 + from dataclasses import dataclass, field 8 + from typing import Any 9 + from urllib.parse import parse_qs, urlparse 10 + 11 + import httpx 12 + 13 + 14 + class CaseInsensitiveDict(dict): 15 + """A dict that allows case-insensitive key access.""" 16 + 17 + def __getitem__(self, key: str) -> str: 18 + # Try exact match first 19 + try: 20 + return super().__getitem__(key) 21 + except KeyError: 22 + pass 23 + # Try case-insensitive match 24 + key_lower = key.lower() 25 + for k, v in self.items(): 26 + if k.lower() == key_lower: 27 + return v 28 + raise KeyError(key) 29 + 30 + def __contains__(self, key: object) -> bool: 31 + if super().__contains__(key): 32 + return True 33 + if isinstance(key, str): 34 + key_lower = key.lower() 35 + return any(k.lower() == key_lower for k in self.keys()) 36 + return False 37 + 38 + def get(self, key: str, default: str | None = None) -> str | None: 39 + try: 40 + return self[key] 41 + except KeyError: 42 + return default 43 + 44 + 45 + @dataclass 46 + class RecordedRequest: 47 + """Captured request for inspection.""" 48 + 49 + method: str 50 + url: str 51 + _headers: dict[str, str] 52 + body: bytes 53 + 54 + @property 55 + def headers(self) -> CaseInsensitiveDict: 56 + """Return headers with case-insensitive access.""" 57 + return CaseInsensitiveDict(self._headers) 58 + 59 + @property 60 + def querystring(self) -> dict[str, list[str]]: 61 + """Parse query string from URL.""" 62 + parsed = urlparse(self.url) 63 + return parse_qs(parsed.query) 64 + 65 + def json(self) -> Any: 66 + """Parse body as JSON.""" 67 + return json.loads(self.body.decode("utf-8")) 68 + 69 + 70 + @dataclass 71 + class MockRoute: 72 + """A mocked route definition.""" 73 + 74 + method: str 75 + url: str 76 + body: str | bytes | Callable = "" 77 + status: int = 200 78 + content_type: str = "application/json" 79 + headers: dict[str, str] = field(default_factory=dict) 80 + 81 + def matches(self, request: httpx.Request) -> bool: 82 + """Check if this route matches the request.""" 83 + if request.method != self.method: 84 + return False 85 + request_url = str(request.url).split("?")[0] 86 + route_url = self.url.split("?")[0] 87 + return request_url == route_url 88 + 89 + def respond( 90 + self, request: httpx.Request, recorded: RecordedRequest 91 + ) -> httpx.Response: 92 + """Generate response for this route.""" 93 + # Ensure all header values are strings 94 + response_headers = {"content-type": self.content_type} 95 + for k, v in self.headers.items(): 96 + response_headers[k] = str(v) 97 + 98 + if callable(self.body): 99 + # httpretty callback: (request, uri, headers) -> [status, headers, body] 100 + result = self.body(recorded, str(request.url), response_headers) 101 + status, resp_headers, body = result 102 + if isinstance(body, str): 103 + body = body.encode("utf-8") 104 + # Ensure callback response headers are strings too 105 + resp_headers = {k: str(v) for k, v in resp_headers.items()} 106 + return httpx.Response(status, headers=resp_headers, content=body) 107 + 108 + body = self.body 109 + if isinstance(body, str): 110 + body = body.encode("utf-8") 111 + return httpx.Response(self.status, headers=response_headers, content=body) 112 + 113 + 114 + class MockClient: 115 + """Mock HTTP client with request recording.""" 116 + 117 + def __init__(self): 118 + self.routes: list[MockRoute] = [] 119 + self.requests: list[RecordedRequest] = [] 120 + self._client: httpx.Client | None = None 121 + 122 + @property 123 + def client(self) -> httpx.Client: 124 + """Get the httpx Client with mock transport.""" 125 + if self._client is None: 126 + self._client = httpx.Client( 127 + transport=httpx.MockTransport(self._handle), 128 + follow_redirects=True, 129 + ) 130 + return self._client 131 + 132 + def register( 133 + self, 134 + method: str, 135 + url: str, 136 + body: str | bytes | Callable = "", 137 + status: int = 200, 138 + content_type: str = "application/json", 139 + headers: dict[str, Any] | None = None, 140 + ) -> None: 141 + """Register a mock route.""" 142 + # Convert header values to strings 143 + str_headers = {} 144 + if headers: 145 + str_headers = {k: str(v) for k, v in headers.items()} 146 + 147 + self.routes.append( 148 + MockRoute( 149 + method=method, 150 + url=url, 151 + body=body, 152 + status=status, 153 + content_type=content_type, 154 + headers=str_headers, 155 + ) 156 + ) 157 + 158 + def reset(self) -> None: 159 + """Clear all routes and recorded requests.""" 160 + self.routes.clear() 161 + self.requests.clear() 162 + 163 + def last_request(self) -> RecordedRequest | None: 164 + """Get the last recorded request.""" 165 + return self.requests[-1] if self.requests else None 166 + 167 + def latest_requests(self) -> list[RecordedRequest]: 168 + """Get all recorded requests.""" 169 + return self.requests.copy() 170 + 171 + def _handle(self, request: httpx.Request) -> httpx.Response: 172 + """Handle an HTTP request.""" 173 + recorded = RecordedRequest( 174 + method=request.method, 175 + url=str(request.url), 176 + _headers=dict(request.headers), 177 + body=request.content, 178 + ) 179 + self.requests.append(recorded) 180 + 181 + for route in reversed(self.routes): 182 + if route.matches(request): 183 + return route.respond(request, recorded) 184 + 185 + return httpx.Response(404, content=b"Not Found")
-1
tests/test_async.py
··· 5 5 from pyzotero.filetransport import AsyncClient 6 6 7 7 # ruff: noqa: PLR2004 8 - # ruff: noqa: S101 9 8 10 9 11 10 @pytest.mark.asyncio
+303 -302
tests/test_zotero.py
··· 11 11 import time 12 12 import unittest 13 13 from unittest.mock import MagicMock, patch 14 - from urllib.parse import parse_qs, urlparse 14 + from urllib.parse import parse_qs, urlencode, urlparse 15 15 16 - import httpretty 17 16 import pytz 18 17 import whenever 19 18 from dateutil import parser 20 - from httpretty import HTTPretty 19 + 20 + from .mock_client import MockClient 21 21 22 22 try: 23 23 from pyzotero.pyzotero import zotero as z ··· 25 25 except ModuleNotFoundError: 26 26 from pyzotero import zotero as z 27 27 from pyzotero.zotero import DEFAULT_ITEM_LIMIT 28 - from urllib.parse import urlencode 29 28 30 29 31 30 class ZoteroTests(unittest.TestCase): ··· 59 58 self.creation_doc = self.get_doc("creation_doc.json") 60 59 self.item_file = self.get_doc("item_file.pdf") 61 60 62 - # Add the item file to the mock response by default 63 - HTTPretty.enable() 64 - HTTPretty.register_uri( 65 - HTTPretty.GET, 66 - "https://api.zotero.org/users/myuserID/items", 67 - content_type="application/json", 68 - body=self.items_doc, 69 - ) 70 - 71 61 def testBuildUrlCorrectHandleEndpoint(self): 72 62 """Url should be concat correctly by build_url""" 73 63 url = z.build_url("http://localhost:23119/api", "/users/0") ··· 75 65 url = z.build_url("http://localhost:23119/api/", "/users/0") 76 66 self.assertEqual(url, "http://localhost:23119/api/users/0") 77 67 78 - @httpretty.activate 79 68 def testFailWithoutCredentials(self): 80 69 """Instance creation should fail, because we're leaving out a 81 70 credential 82 71 """ 72 + mock = MockClient() 83 73 with self.assertRaises(z.ze.MissingCredentialsError): 84 - z.Zotero("myuserID") 74 + z.Zotero("myuserID", client=mock.client) 85 75 86 - @httpretty.activate 87 76 def testRequestBuilder(self): 88 77 """Should url-encode all added parameters""" 89 - zot = z.Zotero("myuserID", "user", "myuserkey") 78 + mock = MockClient() 79 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 90 80 zot.add_parameters(limit=0, start=7) 91 81 self.assertEqual( 92 82 parse_qs(f"start=7&limit={DEFAULT_ITEM_LIMIT}&format=json"), 93 - parse_qs(urlencode(zot.url_params, doseq=True)), 83 + parse_qs(urlencode(zot.url_params or {}, doseq=True)), 94 84 ) 95 85 96 - @httpretty.activate 97 86 def testLocale(self): 98 87 """Should correctly add locale to request because it's an initial request""" 99 - HTTPretty.register_uri( 100 - HTTPretty.GET, 88 + mock = MockClient() 89 + mock.register( 90 + "GET", 101 91 "https://api.zotero.org/users/myuserID/items", 102 92 content_type="application/json", 103 93 body=self.item_doc, 104 94 ) 105 - zot = z.Zotero("myuserID", "user", "myuserkey") 95 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 106 96 _ = zot.items() 107 97 req = zot.request 108 98 self.assertIn("locale=en-US", str(req.url)) 109 99 110 - @httpretty.activate 111 100 def testLocalePreservedWithMethodParams(self): 112 101 """Should preserve locale when methods provide their own parameters""" 113 - HTTPretty.register_uri( 114 - HTTPretty.GET, 102 + mock = MockClient() 103 + mock.register( 104 + "GET", 115 105 "https://api.zotero.org/users/myuserID/items/top", 116 106 content_type="application/json", 117 107 body=self.items_doc, 118 108 ) 119 109 # Test with non-default locale 120 - zot = z.Zotero("myuserID", "user", "myuserkey", locale="de-DE") 110 + zot = z.Zotero( 111 + "myuserID", "user", "myuserkey", locale="de-DE", client=mock.client 112 + ) 121 113 # Call top() with limit which internally adds parameters 122 114 _ = zot.top(limit=1) 123 115 req = zot.request ··· 126 118 # Also verify the method parameter is present 127 119 self.assertIn("limit=1", str(req.url)) 128 120 129 - @httpretty.activate 130 121 def testRequestBuilderLimitNone(self): 131 122 """Should skip limit = 100 param if limit is set to None""" 132 - zot = z.Zotero("myuserID", "user", "myuserkey") 123 + mock = MockClient() 124 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 133 125 zot.add_parameters(limit=None, start=7) 134 126 self.assertEqual( 135 - parse_qs("start=7&format=json"), parse_qs(urlencode(zot.url_params)) 127 + parse_qs("start=7&format=json"), parse_qs(urlencode(zot.url_params or {})) 136 128 ) 137 129 138 - @httpretty.activate 139 130 def testRequestBuilderLimitNegativeOne(self): 140 131 """Should skip limit = 100 param if limit is set to -1""" 141 - zot = z.Zotero("myuserID", "user", "myuserkey") 132 + mock = MockClient() 133 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 142 134 zot.add_parameters(limit=-1, start=7) 143 135 self.assertEqual( 144 136 parse_qs("start=7&format=json"), 145 - parse_qs(urlencode(zot.url_params, doseq=True)), 137 + parse_qs(urlencode(zot.url_params or {}, doseq=True)), 146 138 ) 147 139 148 140 # @httpretty.activate ··· 159 151 # sorted(parse_qs(orig).items()), 160 152 # sorted(parse_qs(query).items())) 161 153 162 - @httpretty.activate 163 154 def testParseItemJSONDoc(self): 164 155 """Should successfully return a list of item dicts, key should match 165 156 input doc's zapi:key value, and author should have been correctly 166 157 parsed out of the XHTML payload 167 158 """ 168 - zot = z.Zotero("myuserID", "user", "myuserkey") 169 - HTTPretty.register_uri( 170 - HTTPretty.GET, 159 + mock = MockClient() 160 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 161 + mock.register( 162 + "GET", 171 163 "https://api.zotero.org/users/myuserID/items", 172 164 content_type="application/json", 173 165 body=self.item_doc, ··· 183 175 incoming_dt = parser.parse(items_data["data"]["dateModified"]) 184 176 self.assertEqual(test_dt, incoming_dt) 185 177 186 - @httpretty.activate 187 178 def testBackoff(self): 188 179 """Test that backoffs are correctly processed""" 189 - zot = z.Zotero("myuserID", "user", "myuserkey") 190 - HTTPretty.register_uri( 191 - HTTPretty.GET, 180 + mock = MockClient() 181 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 182 + mock.register( 183 + "GET", 192 184 "https://api.zotero.org/users/myuserID/items", 193 185 content_type="application/json", 194 186 body=self.item_doc, 195 - adding_headers={"backoff": 0.2}, 187 + headers={"backoff": 0.2}, 196 188 ) 197 189 zot.items() 198 190 # backoff_until should be in the future ··· 201 193 # backoff_until should now be in the past 202 194 self.assertLess(zot.backoff_until, time.time()) 203 195 204 - @httpretty.activate 205 196 def testGetItemFile(self): 206 197 """ 207 198 Should successfully return a binary string with a PDF content 208 199 """ 209 - zot = z.Zotero("myuserid", "user", "myuserkey") 210 - HTTPretty.register_uri( 211 - HTTPretty.GET, 200 + mock = MockClient() 201 + zot = z.Zotero("myuserid", "user", "myuserkey", client=mock.client) 202 + mock.register( 203 + "GET", 212 204 "https://api.zotero.org/users/myuserid/items/MYITEMID/file", 213 205 content_type="application/pdf", 214 206 body=self.item_file, ··· 216 208 items_data = zot.file("myitemid") 217 209 self.assertEqual(b"One very strange PDF\n", items_data) 218 210 219 - @httpretty.activate 220 211 def testParseAttachmentsJSONDoc(self): 221 212 """Ensure that attachments are being correctly parsed""" 222 - zot = z.Zotero("myuserid", "user", "myuserkey") 223 - HTTPretty.register_uri( 224 - HTTPretty.GET, 213 + mock = MockClient() 214 + zot = z.Zotero("myuserid", "user", "myuserkey", client=mock.client) 215 + mock.register( 216 + "GET", 225 217 "https://api.zotero.org/users/myuserid/items", 226 218 content_type="application/json", 227 219 body=self.attachments_doc, ··· 229 221 attachments_data = zot.items() 230 222 self.assertEqual("1641 Depositions", attachments_data["data"]["title"]) 231 223 232 - @httpretty.activate 233 224 def testParseKeysResponse(self): 234 225 """Check that parsing plain keys returned by format = keys works""" 235 - zot = z.Zotero("myuserid", "user", "myuserkey") 226 + mock = MockClient() 227 + zot = z.Zotero("myuserid", "user", "myuserkey", client=mock.client) 236 228 zot.url_params = {"format": "keys"} 237 - HTTPretty.register_uri( 238 - HTTPretty.GET, 229 + mock.register( 230 + "GET", 239 231 "https://api.zotero.org/users/myuserid/items?format=keys", 240 232 content_type="text/plain", 241 233 body=self.keys_response, ··· 243 235 response = zot.items() 244 236 self.assertEqual("JIFWQ4AN", response[:8].decode("utf-8")) 245 237 246 - @httpretty.activate 247 238 def testParseItemVersionsResponse(self): 248 239 """Check that parsing version dict returned by format = versions works""" 249 - zot = z.Zotero("myuserid", "user", "myuserkey") 250 - HTTPretty.register_uri( 251 - HTTPretty.GET, 240 + mock = MockClient() 241 + zot = z.Zotero("myuserid", "user", "myuserkey", client=mock.client) 242 + mock.register( 243 + "GET", 252 244 "https://api.zotero.org/users/myuserid/items?format=versions", 253 245 content_type="application/json", 254 246 body=self.item_versions, ··· 258 250 self.assertEqual(iversions["EAWCSKSF"], 4087) 259 251 self.assertEqual(len(iversions), 2) 260 252 261 - @httpretty.activate 262 253 def testParseCollectionVersionsResponse(self): 263 254 """Check that parsing version dict returned by format = versions works""" 264 - zot = z.Zotero("myuserid", "user", "myuserkey") 265 - HTTPretty.register_uri( 266 - HTTPretty.GET, 255 + mock = MockClient() 256 + zot = z.Zotero("myuserid", "user", "myuserkey", client=mock.client) 257 + mock.register( 258 + "GET", 267 259 "https://api.zotero.org/users/myuserid/collections?format=versions", 268 260 content_type="application/json", 269 261 body=self.collection_versions, ··· 273 265 self.assertEqual(iversions["EAWCSKSF"], 4087) 274 266 self.assertEqual(len(iversions), 2) 275 267 276 - @httpretty.activate 277 268 def testParseChildItems(self): 278 269 """Try and parse child items""" 279 - zot = z.Zotero("myuserid", "user", "myuserkey") 280 - HTTPretty.register_uri( 281 - HTTPretty.GET, 270 + mock = MockClient() 271 + zot = z.Zotero("myuserid", "user", "myuserkey", client=mock.client) 272 + mock.register( 273 + "GET", 282 274 "https://api.zotero.org/users/myuserid/items/ABC123/children", 283 275 content_type="application/json", 284 276 body=self.items_doc, ··· 286 278 items_data = zot.children("ABC123") 287 279 self.assertEqual("NM66T6EF", items_data[0]["key"]) 288 280 289 - @httpretty.activate 290 281 def testCitUTF8(self): 291 282 """Ensure that unicode citations are correctly processed by Pyzotero""" 292 - zot = z.Zotero("myuserID", "user", "myuserkey") 283 + mock = MockClient() 284 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 293 285 url = "https://api.zotero.org/users/myuserID/items/GW8V2CK7" 294 - HTTPretty.register_uri( 295 - HTTPretty.GET, 286 + mock.register( 287 + "GET", 296 288 url, 297 289 content_type="application/atom+xml", 298 290 body=self.citation_doc, ··· 317 309 # u'<div class="csl-entry">Robert A. Caro. \u201cThe Power Broker\u202f: Robert Moses and the Fall of New York,\u201d 1974.</div>' 318 310 # ) 319 311 320 - @httpretty.activate 321 312 def testParseCollectionJSONDoc(self): 322 313 """Should successfully return a single collection dict, 323 314 'name' key value should match input doc's name value 324 315 """ 325 - zot = z.Zotero("myuserID", "user", "myuserkey") 326 - HTTPretty.register_uri( 327 - HTTPretty.GET, 316 + mock = MockClient() 317 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 318 + mock.register( 319 + "GET", 328 320 "https://api.zotero.org/users/myuserID/collections/KIMI8BSG", 329 321 content_type="application/json", 330 322 body=self.collection_doc, ··· 332 324 collections_data = zot.collection("KIMI8BSG") 333 325 self.assertEqual("LoC", collections_data["data"]["name"]) 334 326 335 - @httpretty.activate 336 327 def testParseCollectionTagsJSONDoc(self): 337 328 """Should successfully return a list of tags, 338 329 which should match input doc's number of tag items and 'tag' values 339 330 """ 340 - zot = z.Zotero("myuserID", "user", "myuserkey") 341 - HTTPretty.register_uri( 342 - HTTPretty.GET, 331 + mock = MockClient() 332 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 333 + mock.register( 334 + "GET", 343 335 "https://api.zotero.org/users/myuserID/collections/KIMI8BSG/tags", 344 336 content_type="application/json", 345 337 body=self.collection_tags, ··· 349 341 for item in collections_data: 350 342 self.assertTrue(item in ["apple", "banana", "cherry"]) 351 343 352 - @httpretty.activate 353 344 def testParseCollectionsJSONDoc(self): 354 345 """Should successfully return a list of collection dicts, key should 355 346 match input doc's zapi:key value, and 'title' value should match 356 347 input doc's title value 357 348 """ 358 - zot = z.Zotero("myuserID", "user", "myuserkey") 359 - HTTPretty.register_uri( 360 - HTTPretty.GET, 349 + mock = MockClient() 350 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 351 + mock.register( 352 + "GET", 361 353 "https://api.zotero.org/users/myuserID/collections", 362 354 content_type="application/json", 363 355 body=self.collections_doc, ··· 365 357 collections_data = zot.collections() 366 358 self.assertEqual("LoC", collections_data[0]["data"]["name"]) 367 359 368 - @httpretty.activate 369 360 def testParseTagsJSON(self): 370 361 """Should successfully return a list of tags""" 371 - zot = z.Zotero("myuserID", "user", "myuserkey") 372 - HTTPretty.register_uri( 373 - HTTPretty.GET, 362 + mock = MockClient() 363 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 364 + mock.register( 365 + "GET", 374 366 "https://api.zotero.org/users/myuserID/tags?limit=1", 375 367 content_type="application/json", 376 368 body=self.tags_doc, ··· 378 370 tags_data = zot.tags() 379 371 self.assertEqual("Community / Economic Development", tags_data[0]) 380 372 381 - @httpretty.activate 382 373 def testUrlBuild(self): 383 374 """Ensure that URL parameters are successfully encoded by the http library""" 384 - zot = z.Zotero("myuserID", "user", "myuserkey") 385 - HTTPretty.register_uri( 386 - HTTPretty.GET, 375 + mock = MockClient() 376 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 377 + mock.register( 378 + "GET", 387 379 "https://api.zotero.org/users/myuserID/tags?limit=1", 388 380 content_type="application/json", 389 381 body=self.tags_doc, ··· 398 390 url_str.startswith("https://api.zotero.org/users/myuserID/tags?") 399 391 ) 400 392 401 - @httpretty.activate 402 393 def testParseLinkHeaders(self): 403 394 """Should successfully parse link headers""" 404 - zot = z.Zotero("myuserID", "user", "myuserkey") 405 - HTTPretty.register_uri( 406 - HTTPretty.GET, 395 + mock = MockClient() 396 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 397 + mock.register( 398 + "GET", 407 399 "https://api.zotero.org/users/myuserID/tags?limit=1", 408 400 content_type="application/json", 409 401 body=self.tags_doc, 410 - adding_headers={ 402 + headers={ 411 403 "Link": '<https://api.zotero.org/users/436/items/top?limit=1&start=1>; rel="next", <https://api.zotero.org/users/436/items/top?limit=1&start=2319>; rel="last", <https://www.zotero.org/users/436/items/top>; rel="alternate"' 412 404 }, 413 405 ) 414 406 zot.tags() 407 + assert zot.links is not None 415 408 self.assertEqual(zot.links["next"], "/users/436/items/top?limit=1&start=1") 416 409 self.assertEqual(zot.links["last"], "/users/436/items/top?limit=1&start=2319") 417 410 self.assertEqual(zot.links["alternate"], "/users/436/items/top") 418 411 419 - @httpretty.activate 420 412 def testParseLinkHeadersPreservesAllParameters(self): 421 413 """Test that the self link preserves all parameters, not just the first 2""" 422 - zot = z.Zotero("myuserID", "user", "myuserkey") 423 - HTTPretty.register_uri( 424 - HTTPretty.GET, 414 + mock = MockClient() 415 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 416 + mock.register( 417 + "GET", 425 418 "https://api.zotero.org/users/myuserID/items/top", 426 419 content_type="application/json", 427 420 body=self.items_doc, 428 - adding_headers={ 421 + headers={ 429 422 "Link": '<https://api.zotero.org/users/myuserID/items/top?start=1>; rel="next"' 430 423 }, 431 424 ) 432 425 # Call with multiple parameters including limit 433 426 zot.top(limit=1) 434 427 # The self link should preserve all parameters except format 428 + assert zot.links is not None 435 429 self.assertIn("limit=1", zot.links["self"]) 436 430 self.assertIn("locale=", zot.links["self"]) 437 431 # format should be stripped 438 432 self.assertNotIn("format=", zot.links["self"]) 439 433 440 - @httpretty.activate 441 434 def testParseGroupsJSONDoc(self): 442 435 """Should successfully return a list of group dicts, ID should match 443 436 input doc's zapi:key value, and 'total_items' value should match 444 437 input doc's zapi:numItems value 445 438 """ 446 - zot = z.Zotero("myuserID", "user", "myuserkey") 447 - HTTPretty.register_uri( 448 - HTTPretty.GET, 439 + mock = MockClient() 440 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 441 + mock.register( 442 + "GET", 449 443 "https://api.zotero.org/users/myuserID/groups", 450 444 content_type="application/json", 451 445 body=self.groups_doc, ··· 462 456 # Should get default limit=100 since no limit specified in second call 463 457 self.assertEqual( 464 458 parse_qs(f"start=2&format=json&limit={DEFAULT_ITEM_LIMIT}"), 465 - parse_qs(urlencode(zot.url_params, doseq=True)), 459 + parse_qs(urlencode(zot.url_params or {}, doseq=True)), 466 460 ) 467 461 468 - @httpretty.activate 469 462 def testParamsBlankAfterCall(self): 470 463 """self.url_params should be blank after an API call""" 471 - zot = z.Zotero("myuserID", "user", "myuserkey") 472 - HTTPretty.register_uri( 473 - HTTPretty.GET, 464 + mock = MockClient() 465 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 466 + mock.register( 467 + "GET", 474 468 "https://api.zotero.org/users/myuserID/items", 475 469 content_type="application/json", 476 470 body=self.items_doc, ··· 478 472 zot.items() 479 473 self.assertEqual(None, zot.url_params) 480 474 481 - @httpretty.activate 482 475 def testResponseForbidden(self): 483 476 """Ensure that an error is properly raised for 403""" 484 - zot = z.Zotero("myuserID", "user", "myuserkey") 485 - HTTPretty.register_uri( 486 - HTTPretty.GET, 477 + mock = MockClient() 478 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 479 + mock.register( 480 + "GET", 487 481 "https://api.zotero.org/users/myuserID/items", 488 482 content_type="application/json", 489 483 body=self.items_doc, ··· 492 486 with self.assertRaises(z.ze.UserNotAuthorisedError): 493 487 zot.items() 494 488 495 - @httpretty.activate 496 489 def testTimeout(self): 497 490 """Ensure that an error is properly raised for 503""" 498 - zot = z.Zotero("myuserID", "user", "myuserkey") 499 - HTTPretty.register_uri( 500 - HTTPretty.GET, 491 + mock = MockClient() 492 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 493 + mock.register( 494 + "GET", 501 495 "https://api.zotero.org/users/myuserID/items", 502 496 content_type="application/json", 503 497 body=self.items_doc, ··· 506 500 with self.assertRaises(z.ze.HTTPError): 507 501 zot.items() 508 502 509 - @httpretty.activate 510 503 def testResponseUnsupported(self): 511 504 """Ensure that an error is properly raised for 400""" 512 - zot = z.Zotero("myuserID", "user", "myuserkey") 513 - HTTPretty.register_uri( 514 - HTTPretty.GET, 505 + mock = MockClient() 506 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 507 + mock.register( 508 + "GET", 515 509 "https://api.zotero.org/users/myuserID/items", 516 510 content_type="application/json", 517 511 body=self.items_doc, ··· 520 514 with self.assertRaises(z.ze.UnsupportedParamsError): 521 515 zot.items() 522 516 523 - @httpretty.activate 524 517 def testResponseNotFound(self): 525 518 """Ensure that an error is properly raised for 404""" 526 - zot = z.Zotero("myuserID", "user", "myuserkey") 527 - HTTPretty.register_uri( 528 - HTTPretty.GET, 519 + mock = MockClient() 520 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 521 + mock.register( 522 + "GET", 529 523 "https://api.zotero.org/users/myuserID/items", 530 524 body=self.items_doc, 531 525 content_type="application/json", ··· 534 528 with self.assertRaises(z.ze.ResourceNotFoundError): 535 529 zot.items() 536 530 537 - @httpretty.activate 538 531 def testResponseMiscError(self): 539 532 """Ensure that an error is properly raised for unspecified errors""" 540 - zot = z.Zotero("myuserID", "user", "myuserkey") 541 - HTTPretty.register_uri( 542 - HTTPretty.GET, 533 + mock = MockClient() 534 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 535 + mock.register( 536 + "GET", 543 537 "https://api.zotero.org/users/myuserID/items", 544 538 content_type="application/json", 545 539 body=self.items_doc, ··· 548 542 with self.assertRaises(z.ze.HTTPError): 549 543 zot.items() 550 544 551 - @httpretty.activate 552 545 def testGetItems(self): 553 546 """Ensure that we can retrieve a list of all items""" 554 - zot = z.Zotero("myuserID", "user", "myuserkey") 555 - HTTPretty.register_uri( 556 - HTTPretty.GET, 547 + mock = MockClient() 548 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 549 + mock.register( 550 + "GET", 557 551 "https://api.zotero.org/itemTypes", 558 552 content_type="application/json", 559 553 body=self.item_types, ··· 561 555 resp = zot.item_types() 562 556 self.assertEqual(resp[0]["itemType"], "artwork") 563 557 564 - @httpretty.activate 565 558 def testGetTemplate(self): 566 559 """Ensure that item templates are retrieved and converted into dicts""" 567 - zot = z.Zotero("myuserID", "user", "myuserkey") 568 - HTTPretty.register_uri( 569 - HTTPretty.GET, 560 + mock = MockClient() 561 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 562 + mock.register( 563 + "GET", 570 564 "https://api.zotero.org/items/new?itemType=book", 571 565 content_type="application/json", 572 566 body=self.item_templt, ··· 581 575 with self.assertRaises(z.ze.ParamNotPassedError): 582 576 t = zot.create_collections(t) 583 577 584 - @httpretty.activate 585 578 def testNoApiKey(self): 586 579 """Ensure that pyzotero works when api_key is not set""" 587 - zot = z.Zotero("myuserID", "user") 588 - HTTPretty.register_uri( 589 - HTTPretty.GET, 580 + mock = MockClient() 581 + zot = z.Zotero("myuserID", "user", client=mock.client) 582 + mock.register( 583 + "GET", 590 584 "https://api.zotero.org/users/myuserID/items", 591 585 content_type="application/json", 592 586 body=self.item_doc, ··· 612 606 # items_data['title'] = 'flibble' 613 607 # json.dumps(*zot._cleanup(items_data)) 614 608 615 - @httpretty.activate 616 609 def testCollectionCreation(self): 617 610 """Tests creation of a new collection""" 618 - zot = z.Zotero("myuserID", "user", "myuserkey") 619 - HTTPretty.register_uri( 620 - HTTPretty.POST, 611 + mock = MockClient() 612 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 613 + mock.register( 614 + "POST", 621 615 "https://api.zotero.org/users/myuserID/collections", 622 616 body=self.creation_doc, 623 617 content_type="application/json", ··· 626 620 # now let's test something 627 621 resp = zot.create_collections([{"name": "foo", "key": "ABC123"}]) 628 622 self.assertTrue("ABC123", resp["success"]["0"]) 629 - request = httpretty.last_request() 623 + request = mock.last_request() 630 624 self.assertFalse("If-Unmodified-Since-Version" in request.headers) 631 625 632 - @httpretty.activate 633 626 def testCollectionCreationLastModified(self): 634 627 """Tests creation of a new collection with last_modified param""" 635 - zot = z.Zotero("myuserID", "user", "myuserkey") 636 - HTTPretty.register_uri( 637 - HTTPretty.POST, 628 + mock = MockClient() 629 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 630 + mock.register( 631 + "POST", 638 632 "https://api.zotero.org/users/myuserID/collections", 639 633 body=self.creation_doc, 640 634 content_type="application/json", ··· 645 639 [{"name": "foo", "key": "ABC123"}], last_modified=5 646 640 ) 647 641 self.assertEqual("ABC123", resp["success"]["0"]) 648 - request = httpretty.last_request() 642 + request = mock.last_request() 649 643 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "5") 650 644 651 - @httpretty.activate 652 645 def testCollectionUpdate(self): 653 646 """Tests update of a collection""" 654 - zot = z.Zotero("myuserID", "user", "myuserkey") 655 - HTTPretty.register_uri( 656 - HTTPretty.PUT, 647 + mock = MockClient() 648 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 649 + mock.register( 650 + "PUT", 657 651 "https://api.zotero.org/users/myuserID/collections/ABC123", 658 652 body="", 659 653 content_type="application/json", ··· 662 656 # now let's test something 663 657 resp = zot.update_collection({"name": "foo", "key": "ABC123", "version": 3}) 664 658 self.assertEqual(True, resp) 665 - request = httpretty.last_request() 659 + request = mock.last_request() 666 660 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "3") 667 661 668 - @httpretty.activate 669 662 def testCollectionUpdateLastModified(self): 670 663 """Tests update of a collection with last_modified set""" 671 - zot = z.Zotero("myuserID", "user", "myuserkey") 672 - HTTPretty.register_uri( 673 - HTTPretty.PUT, 664 + mock = MockClient() 665 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 666 + mock.register( 667 + "PUT", 674 668 "https://api.zotero.org/users/myuserID/collections/ABC123", 675 669 body="", 676 670 content_type="application/json", ··· 681 675 {"name": "foo", "key": "ABC123", "version": 3}, last_modified=5 682 676 ) 683 677 self.assertEqual(True, resp) 684 - request = httpretty.last_request() 678 + request = mock.last_request() 685 679 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "5") 686 680 687 681 def testItemAttachmentLinkModes(self): ··· 696 690 modes_from_instance = zot.item_attachment_link_modes() 697 691 self.assertEqual(modes, modes_from_instance) 698 692 699 - @httpretty.activate 700 693 def testItemCreation(self): 701 694 """Tests creation of a new item using a template""" 702 - zot = z.Zotero("myuserID", "user", "myuserkey") 703 - HTTPretty.register_uri( 704 - HTTPretty.GET, 695 + mock = MockClient() 696 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 697 + mock.register( 698 + "GET", 705 699 "https://api.zotero.org/items/new?itemType=book", 706 700 body=self.item_templt, 707 701 content_type="application/json", 708 702 ) 709 703 template = zot.item_template("book") 710 - httpretty.reset() 711 - HTTPretty.register_uri( 712 - HTTPretty.POST, 704 + mock.reset() 705 + mock.register( 706 + "POST", 713 707 "https://api.zotero.org/users/myuserID/items", 714 708 body=self.creation_doc, 715 709 content_type="application/json", ··· 718 712 # now let's test something 719 713 resp = zot.create_items([template]) 720 714 self.assertEqual("ABC123", resp["success"]["0"]) 721 - request = httpretty.last_request() 715 + request = mock.last_request() 722 716 self.assertFalse("If-Unmodified-Since-Version" in request.headers) 723 717 724 - @httpretty.activate 725 718 def testItemCreationLastModified(self): 726 719 """Checks 'If-Unmodified-Since-Version' header correctly set on create_items""" 727 - zot = z.Zotero("myuserID", "user", "myuserkey") 728 - HTTPretty.register_uri( 729 - HTTPretty.POST, 720 + mock = MockClient() 721 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 722 + mock.register( 723 + "POST", 730 724 "https://api.zotero.org/users/myuserID/items", 731 725 body=self.creation_doc, 732 726 content_type="application/json", ··· 734 728 ) 735 729 # now let's test something 736 730 zot.create_items([{"key": "ABC123"}], last_modified=5) 737 - request = httpretty.last_request() 731 + request = mock.last_request() 738 732 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "5") 739 733 740 - @httpretty.activate 741 734 def testItemUpdate(self): 742 735 """Tests item update using update_item""" 743 - zot = z.Zotero("myuserID", "user", "myuserkey") 736 + mock = MockClient() 737 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 744 738 update = {"key": "ABC123", "version": 3, "itemType": "book"} 745 - HTTPretty.register_uri( 746 - HTTPretty.GET, 739 + mock.register( 740 + "GET", 747 741 "https://api.zotero.org/itemFields", 748 742 body=self.item_fields, 749 743 content_type="application/json", 750 744 ) 751 - HTTPretty.register_uri( 752 - HTTPretty.PATCH, 745 + mock.register( 746 + "PATCH", 753 747 "https://api.zotero.org/users/myuserID/items/ABC123", 754 748 body="", 755 749 content_type="application/json", ··· 758 752 # now let's test something 759 753 resp = zot.update_item(update) 760 754 self.assertEqual(resp, True) 761 - request = httpretty.last_request() 755 + request = mock.last_request() 762 756 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "3") 763 757 764 - @httpretty.activate 765 758 def testItemUpdateLastModified(self): 766 759 """Tests item update using update_item with last_modified parameter""" 767 - zot = z.Zotero("myuserID", "user", "myuserkey") 760 + mock = MockClient() 761 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 768 762 update = {"key": "ABC123", "version": 3, "itemType": "book"} 769 - HTTPretty.register_uri( 770 - HTTPretty.GET, 763 + mock.register( 764 + "GET", 771 765 "https://api.zotero.org/itemFields", 772 766 body=self.item_fields, 773 767 content_type="application/json", 774 768 ) 775 - HTTPretty.register_uri( 776 - HTTPretty.PATCH, 769 + mock.register( 770 + "PATCH", 777 771 "https://api.zotero.org/users/myuserID/items/ABC123", 778 772 body="", 779 773 content_type="application/json", ··· 782 776 # now let's test something 783 777 resp = zot.update_item(update, last_modified=5) 784 778 self.assertEqual(resp, True) 785 - request = httpretty.last_request() 779 + request = mock.last_request() 786 780 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "5") 787 781 788 782 def testTooManyItems(self): ··· 792 786 with self.assertRaises(z.ze.TooManyItemsError): 793 787 zot.create_items(itms) 794 788 795 - @httpretty.activate 796 789 def testRateLimitWithBackoff(self): 797 790 """Test 429 response handling when a backoff header is received""" 798 - zot = z.Zotero("myuserID", "user", "myuserkey") 799 - HTTPretty.register_uri( 800 - HTTPretty.GET, 791 + mock = MockClient() 792 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 793 + mock.register( 794 + "GET", 801 795 "https://api.zotero.org/users/myuserID/items", 802 796 status=429, 803 - adding_headers={"backoff": 0.1}, 797 + body="[]", 798 + headers={"backoff": 0.1}, 804 799 ) 805 800 zot.items() 806 801 # backoff_until should be in the future 807 802 self.assertGreater(zot.backoff_until, time.time()) 808 803 809 - @httpretty.activate 810 804 def testDeleteTags(self): 811 805 """Tests deleting tags""" 812 - zot = z.Zotero("myuserID", "user", "myuserkey") 806 + mock = MockClient() 807 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 813 808 814 809 # Mock the initial request to get version info 815 - HTTPretty.register_uri( 816 - HTTPretty.GET, 810 + mock.register( 811 + "GET", 817 812 "https://api.zotero.org/users/myuserID/tags?limit=1", 818 813 content_type="application/json", 819 814 body=self.tags_doc, 820 - adding_headers={"last-modified-version": "42"}, 815 + headers={"last-modified-version": "42"}, 821 816 ) 822 817 823 818 # Mock the delete endpoint 824 - HTTPretty.register_uri( 825 - HTTPretty.DELETE, "https://api.zotero.org/users/myuserID/tags", status=204 819 + mock.register( 820 + "DELETE", "https://api.zotero.org/users/myuserID/tags", status=204 826 821 ) 827 822 828 823 # Test tag deletion 829 824 resp = zot.delete_tags("tag1", "tag2") 830 825 831 826 # Verify the request 832 - request = httpretty.last_request() 827 + request = mock.last_request() 833 828 self.assertEqual(request.method, "DELETE") 834 829 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "42") 835 830 self.assertTrue(resp) 836 831 837 - @httpretty.activate 838 832 def testAddToCollection(self): 839 833 """Tests adding an item to a collection""" 840 - zot = z.Zotero("myuserID", "user", "myuserkey") 834 + mock = MockClient() 835 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 841 836 842 837 # Mock the PATCH endpoint for adding to collection 843 - HTTPretty.register_uri( 844 - HTTPretty.PATCH, 838 + mock.register( 839 + "PATCH", 845 840 "https://api.zotero.org/users/myuserID/items/ITEMKEY", 846 841 status=204, 847 842 ) ··· 853 848 resp = zot.addto_collection("COLLECTIONKEY", item) 854 849 855 850 # Verify the request 856 - request = httpretty.last_request() 851 + request = mock.last_request() 857 852 self.assertEqual(request.method, "PATCH") 858 853 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "5") 859 854 ··· 864 859 865 860 self.assertTrue(resp) 866 861 867 - @httpretty.activate 868 862 def testDeleteItem(self): 869 863 """Tests deleting an item""" 870 - zot = z.Zotero("myuserID", "user", "myuserkey") 864 + mock = MockClient() 865 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 871 866 872 867 # Mock the DELETE endpoint 873 - HTTPretty.register_uri( 874 - HTTPretty.DELETE, 868 + mock.register( 869 + "DELETE", 875 870 "https://api.zotero.org/users/myuserID/items/ITEMKEY", 876 871 status=204, 877 872 ) ··· 883 878 resp = zot.delete_item(item) 884 879 885 880 # Verify the request 886 - request = httpretty.last_request() 881 + request = mock.last_request() 887 882 self.assertEqual(request.method, "DELETE") 888 883 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "7") 889 884 890 885 self.assertTrue(resp) 891 886 892 - @httpretty.activate 893 887 def testDeleteMultipleItems(self): 894 888 """Tests deleting multiple items at once""" 895 - zot = z.Zotero("myuserID", "user", "myuserkey") 889 + mock = MockClient() 890 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 896 891 897 892 # Mock the DELETE endpoint for multiple items 898 - HTTPretty.register_uri( 899 - HTTPretty.DELETE, "https://api.zotero.org/users/myuserID/items", status=204 893 + mock.register( 894 + "DELETE", "https://api.zotero.org/users/myuserID/items", status=204 900 895 ) 901 896 902 897 # Create test items ··· 906 901 resp = zot.delete_item(items) 907 902 908 903 # Verify the request 909 - request = httpretty.last_request() 904 + request = mock.last_request() 910 905 self.assertEqual(request.method, "DELETE") 911 906 self.assertEqual(request.headers["If-Unmodified-Since-Version"], "5") 912 907 ··· 919 914 920 915 self.assertTrue(resp) 921 916 922 - @httpretty.activate 923 917 def testFileUpload(self): 924 918 """Tests file upload process with attachments""" 925 - zot = z.Zotero("myuserID", "user", "myuserkey") 919 + mock = MockClient() 920 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 926 921 927 922 # Create a temporary file for testing 928 923 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload_file.txt") ··· 931 926 932 927 # Mock Step 0: Create preliminary item registration 933 928 prelim_response = {"success": {"0": "ITEMKEY123"}} 934 - HTTPretty.register_uri( 935 - HTTPretty.POST, 929 + mock.register( 930 + "POST", 936 931 "https://api.zotero.org/users/myuserID/items", 937 932 content_type="application/json", 938 933 body=json.dumps(prelim_response), ··· 981 976 # Clean up 982 977 os.remove(temp_file_path) 983 978 984 - @httpretty.activate 985 979 def testFileUploadExists(self): 986 980 """Tests file upload process when the file already exists on the server""" 987 - zot = z.Zotero("myuserID", "user", "myuserkey") 981 + mock = MockClient() 982 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 988 983 989 984 # Create a temporary file for testing 990 985 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload_file.txt") ··· 993 988 994 989 # Mock Step 0: Create preliminary item registration 995 990 prelim_response = {"success": {"0": "ITEMKEY123"}} 996 - HTTPretty.register_uri( 997 - HTTPretty.POST, 991 + mock.register( 992 + "POST", 998 993 "https://api.zotero.org/users/myuserID/items", 999 994 content_type="application/json", 1000 995 body=json.dumps(prelim_response), ··· 1034 1029 # Clean up 1035 1030 os.remove(temp_file_path) 1036 1031 1037 - @httpretty.activate 1038 1032 def testFileUploadWithParentItem(self): 1039 1033 """Tests file upload process with a parent item ID""" 1040 - zot = z.Zotero("myuserID", "user", "myuserkey") 1034 + mock = MockClient() 1035 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1041 1036 1042 1037 # Create a temporary file for testing 1043 1038 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload_file.txt") ··· 1046 1041 1047 1042 # Mock Step 0: Create preliminary item registration 1048 1043 prelim_response = {"success": {"0": "ITEMKEY123"}} 1049 - HTTPretty.register_uri( 1050 - HTTPretty.POST, 1044 + mock.register( 1045 + "POST", 1051 1046 "https://api.zotero.org/users/myuserID/items", 1052 1047 content_type="application/json", 1053 1048 body=json.dumps(prelim_response), ··· 1078 1073 } 1079 1074 1080 1075 # Mock Step 1: Get upload authorization 1081 - HTTPretty.register_uri( 1082 - HTTPretty.POST, 1076 + mock.register( 1077 + "POST", 1083 1078 "https://api.zotero.org/users/myuserID/items/ITEMKEY123/file", 1084 1079 content_type="application/json", 1085 1080 body=json.dumps(mock_auth_data), ··· 1106 1101 1107 1102 # Check that the parentItem was added to the payload 1108 1103 # Get the latest request to the items endpoint 1109 - requests = httpretty.latest_requests() 1104 + requests = mock.latest_requests() 1110 1105 item_request = None 1111 1106 for req in requests: 1112 1107 if req.url.endswith("/items"): ··· 1120 1115 # Clean up 1121 1116 os.remove(temp_file_path) 1122 1117 1123 - @httpretty.activate 1124 1118 def testFileUploadFailure(self): 1125 1119 """Tests file upload process when auth step fails""" 1126 - zot = z.Zotero("myuserID", "user", "myuserkey") 1120 + mock = MockClient() 1121 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1127 1122 1128 1123 # Create a temporary file for testing 1129 1124 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload_file.txt") ··· 1132 1127 1133 1128 # Mock Step 0: Create preliminary item registration 1134 1129 prelim_response = {"success": {"0": "ITEMKEY123"}} 1135 - HTTPretty.register_uri( 1136 - HTTPretty.POST, 1130 + mock.register( 1131 + "POST", 1137 1132 "https://api.zotero.org/users/myuserID/items", 1138 1133 content_type="application/json", 1139 1134 body=json.dumps(prelim_response), ··· 1141 1136 ) 1142 1137 1143 1138 # Mock Step 1: Authorization fails with 403 1144 - HTTPretty.register_uri( 1145 - HTTPretty.POST, 1139 + mock.register( 1140 + "POST", 1146 1141 "https://api.zotero.org/users/myuserID/items/ITEMKEY123/file", 1147 1142 status=403, 1148 1143 ) ··· 1170 1165 # Clean up 1171 1166 os.remove(temp_file_path) 1172 1167 1173 - @httpretty.activate 1174 1168 def testFileUploadSetsContentType(self): 1175 1169 """Tests that contentType is automatically set during upload based on file extension""" 1176 - zot = z.Zotero("myuserID", "user", "myuserkey") 1170 + mock = MockClient() 1171 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1177 1172 1178 1173 # Create a temporary PDF file for testing 1179 1174 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload.pdf") ··· 1188 1183 captured_body.append(body) 1189 1184 return [200, response_headers, json.dumps({"success": {"0": "ITEMKEY123"}})] 1190 1185 1191 - HTTPretty.register_uri( 1192 - HTTPretty.POST, 1186 + mock.register( 1187 + "POST", 1193 1188 "https://api.zotero.org/users/myuserID/items", 1194 1189 body=request_callback, 1195 1190 content_type="application/json", ··· 1222 1217 1223 1218 os.remove(temp_file_path) 1224 1219 1225 - @httpretty.activate 1226 1220 def testFileUploadPreservesUserContentType(self): 1227 1221 """Tests that user-provided contentType is not overridden""" 1228 - zot = z.Zotero("myuserID", "user", "myuserkey") 1222 + mock = MockClient() 1223 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1229 1224 1230 1225 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload.txt") 1231 1226 with open(temp_file_path, "w") as f: ··· 1238 1233 captured_body.append(body) 1239 1234 return [200, response_headers, json.dumps({"success": {"0": "ITEMKEY123"}})] 1240 1235 1241 - HTTPretty.register_uri( 1242 - HTTPretty.POST, 1236 + mock.register( 1237 + "POST", 1243 1238 "https://api.zotero.org/users/myuserID/items", 1244 1239 body=request_callback, 1245 1240 content_type="application/json", ··· 1274 1269 1275 1270 os.remove(temp_file_path) 1276 1271 1277 - @httpretty.activate 1278 1272 def testFileUploadWithPreexistingKeys(self): 1279 1273 """Tests file upload process when the payload already contains keys""" 1280 - zot = z.Zotero("myuserID", "user", "myuserkey") 1274 + mock = MockClient() 1275 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1281 1276 1282 1277 # Create a temporary file for testing 1283 1278 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload_file.txt") ··· 1327 1322 # Clean up 1328 1323 os.remove(temp_file_path) 1329 1324 1330 - @httpretty.activate 1331 1325 def testFileUploadInvalidPayload(self): 1332 1326 """Tests file upload process with invalid payload mixing items with and without keys""" 1333 - zot = z.Zotero("myuserID", "user", "myuserkey") 1327 + mock = MockClient() 1328 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1334 1329 1335 1330 # Create a temporary file for testing 1336 1331 temp_file_path = os.path.join(self.cwd, "api_responses", "test_upload_file.txt") ··· 1357 1352 # Clean up 1358 1353 os.remove(temp_file_path) 1359 1354 1360 - @httpretty.activate 1361 1355 def testAttachmentSimple(self): 1362 1356 """Test attachment_simple method with a single file""" 1363 - zot = z.Zotero("myuserID", "user", "myuserkey") 1357 + mock = MockClient() 1358 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1364 1359 1365 1360 # Create a temporary test file 1366 1361 temp_file_path = os.path.join(self.cwd, "api_responses", "test_attachment.txt") ··· 1368 1363 f.write("Test attachment content") 1369 1364 1370 1365 # Mock the item template response 1371 - HTTPretty.register_uri( 1372 - HTTPretty.GET, 1366 + mock.register( 1367 + "GET", 1373 1368 "https://api.zotero.org/items/new?itemType=attachment&linkMode=imported_file", 1374 1369 content_type="application/json", 1375 1370 body=json.dumps({"itemType": "attachment", "linkMode": "imported_file"}), 1376 1371 ) 1377 1372 1378 1373 # Mock the item creation response 1379 - HTTPretty.register_uri( 1380 - HTTPretty.POST, 1374 + mock.register( 1375 + "POST", 1381 1376 "https://api.zotero.org/users/myuserID/items", 1382 1377 content_type="application/json", 1383 1378 body=json.dumps({"success": {"0": "ITEMKEY123"}}), ··· 1405 1400 self.assertEqual(len(result["success"]), 1) 1406 1401 1407 1402 # Verify that the correct attachment template was used 1408 - request = httpretty.last_request() 1403 + request = mock.last_request() 1409 1404 payload = json.loads(request.body.decode("utf-8")) 1410 1405 self.assertEqual(payload[0]["title"], "test_attachment.txt") 1411 1406 self.assertEqual(payload[0]["filename"], temp_file_path) ··· 1413 1408 # Clean up 1414 1409 os.remove(temp_file_path) 1415 1410 1416 - @httpretty.activate 1417 1411 def testAttachmentSimpleWithParent(self): 1418 1412 """Test attachment_simple method with a parent ID""" 1419 - zot = z.Zotero("myuserID", "user", "myuserkey") 1413 + mock = MockClient() 1414 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1420 1415 1421 1416 # Create a temporary test file 1422 1417 temp_file_path = os.path.join(self.cwd, "api_responses", "test_attachment.txt") ··· 1424 1419 f.write("Test attachment content") 1425 1420 1426 1421 # Mock the item template response 1427 - HTTPretty.register_uri( 1428 - HTTPretty.GET, 1422 + mock.register( 1423 + "GET", 1429 1424 "https://api.zotero.org/items/new?itemType=attachment&linkMode=imported_file", 1430 1425 content_type="application/json", 1431 1426 body=json.dumps({"itemType": "attachment", "linkMode": "imported_file"}), ··· 1458 1453 # Clean up 1459 1454 os.remove(temp_file_path) 1460 1455 1461 - @httpretty.activate 1462 1456 def testAttachmentBoth(self): 1463 1457 """Test attachment_both method with custom title and filename""" 1464 - zot = z.Zotero("myuserID", "user", "myuserkey") 1458 + mock = MockClient() 1459 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1465 1460 1466 1461 # Create a temporary test file 1467 1462 temp_file_path = os.path.join(self.cwd, "api_responses", "test_attachment.txt") ··· 1469 1464 f.write("Test attachment content") 1470 1465 1471 1466 # Mock the item template response 1472 - HTTPretty.register_uri( 1473 - HTTPretty.GET, 1467 + mock.register( 1468 + "GET", 1474 1469 "https://api.zotero.org/items/new?itemType=attachment&linkMode=imported_file", 1475 1470 content_type="application/json", 1476 1471 body=json.dumps({"itemType": "attachment", "linkMode": "imported_file"}), 1477 1472 ) 1478 1473 1479 1474 # Mock the item creation response 1480 - HTTPretty.register_uri( 1481 - HTTPretty.POST, 1475 + mock.register( 1476 + "POST", 1482 1477 "https://api.zotero.org/users/myuserID/items", 1483 1478 content_type="application/json", 1484 1479 body=json.dumps({"success": {"0": "ITEMKEY123"}}), ··· 1508 1503 self.assertEqual(len(result["success"]), 1) 1509 1504 1510 1505 # Verify that the correct attachment template was used 1511 - request = httpretty.last_request() 1506 + request = mock.last_request() 1512 1507 payload = json.loads(request.body.decode("utf-8")) 1513 1508 self.assertEqual(payload[0]["title"], custom_title) 1514 1509 self.assertEqual(payload[0]["filename"], temp_file_path) ··· 1516 1511 # Clean up 1517 1512 os.remove(temp_file_path) 1518 1513 1519 - @httpretty.activate 1520 1514 def testAttachmentBothWithParent(self): 1521 1515 """Test attachment_both method with a parent ID""" 1522 - zot = z.Zotero("myuserID", "user", "myuserkey") 1516 + mock = MockClient() 1517 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1523 1518 1524 1519 # Create a temporary test file 1525 1520 temp_file_path = os.path.join(self.cwd, "api_responses", "test_attachment.txt") ··· 1527 1522 f.write("Test attachment content") 1528 1523 1529 1524 # Mock the item template response 1530 - HTTPretty.register_uri( 1531 - HTTPretty.GET, 1525 + mock.register( 1526 + "GET", 1532 1527 "https://api.zotero.org/items/new?itemType=attachment&linkMode=imported_file", 1533 1528 content_type="application/json", 1534 1529 body=json.dumps({"itemType": "attachment", "linkMode": "imported_file"}), ··· 1565 1560 1566 1561 def tearDown(self): 1567 1562 """Tear stuff down""" 1568 - HTTPretty.disable() 1563 + pass 1569 1564 1570 - @httpretty.activate 1571 1565 def test_updated_template_comparison(self): 1572 1566 """Test that ONE_HOUR is properly used for template freshness check""" 1573 - zot = z.Zotero("myuserID", "user", "myuserkey") 1567 + mock = MockClient() 1568 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1574 1569 1575 1570 # Test that ONE_HOUR constant matches code expectation 1576 1571 self.assertEqual(z.ONE_HOUR, 3600) ··· 1580 1575 1581 1576 # Create a template 1582 1577 template_name = "test_template" 1583 - HTTPretty.register_uri( 1584 - HTTPretty.GET, 1578 + mock.register( 1579 + "GET", 1585 1580 "https://api.zotero.org/users/myuserID/items/new", 1586 1581 body=json.dumps({"success": True}), 1587 1582 content_type="application/json", ··· 1594 1589 self.assertIn(template_name, zot.templates) 1595 1590 self.assertIn("updated", zot.templates[template_name]) 1596 1591 1597 - @httpretty.activate 1598 1592 def test_template_cache_creation(self): 1599 1593 """Test template caching in the _cache method""" 1600 - zot = z.Zotero("myuserID", "user", "myuserkey") 1594 + mock = MockClient() 1595 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1601 1596 1602 1597 # Create a mock response to cache 1603 1598 mock_response = MagicMock() ··· 1619 1614 result["modified"] = True 1620 1615 self.assertNotIn("modified", zot.templates[template_name]["tmplt"]) 1621 1616 1622 - @httpretty.activate 1623 1617 def test_striplocal_local_mode(self): 1624 1618 """Test _striplocal method in local mode""" 1625 - zot = z.Zotero("myuserID", "user", "myuserkey", local=True) 1619 + mock = MockClient() 1620 + zot = z.Zotero("myuserID", "user", "myuserkey", local=True, client=mock.client) 1626 1621 1627 1622 # Test stripping local API path 1628 1623 url = "http://localhost:23119/api/users/myuserID/items" ··· 1636 1631 result, "http://localhost:23119/users/myuserID/collections/ABC123/items" 1637 1632 ) 1638 1633 1639 - @httpretty.activate 1640 1634 def test_striplocal_remote_mode(self): 1641 1635 """Test _striplocal method in remote mode (shouldn't change URL)""" 1642 - zot = z.Zotero("myuserID", "user", "myuserkey", local=False) 1636 + mock = MockClient() 1637 + zot = z.Zotero("myuserID", "user", "myuserkey", local=False, client=mock.client) 1643 1638 1644 1639 # Test without changing URL in remote mode 1645 1640 url = "https://api.zotero.org/users/myuserID/items" 1646 1641 result = zot._striplocal(url) 1647 1642 self.assertEqual(result, url) 1648 1643 1649 - @httpretty.activate 1650 1644 def test_set_fulltext(self): 1651 1645 """Test set_fulltext method for setting full-text data""" 1652 - zot = z.Zotero("myuserID", "user", "myuserkey") 1646 + mock = MockClient() 1647 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1653 1648 1654 1649 # Mock response from Zotero API 1655 - HTTPretty.register_uri( 1656 - HTTPretty.PUT, 1650 + mock.register( 1651 + "PUT", 1657 1652 "https://api.zotero.org/users/myuserID/items/ABCD1234/fulltext", 1658 1653 status=204, 1659 1654 ) ··· 1668 1663 _ = zot.set_fulltext("ABCD1234", pdf_payload) 1669 1664 1670 1665 # Verify the request 1671 - request = httpretty.last_request() 1666 + request = mock.last_request() 1672 1667 self.assertEqual(request.method, "PUT") 1673 1668 self.assertEqual(json.loads(request.body.decode()), pdf_payload) 1674 1669 self.assertEqual(request.headers["Content-Type"], "application/json") 1675 1670 1676 - @httpretty.activate 1677 1671 def test_new_fulltext(self): 1678 1672 """Test new_fulltext method for retrieving newer full-text content""" 1679 - zot = z.Zotero("myuserID", "user", "myuserkey") 1673 + mock = MockClient() 1674 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1680 1675 1681 1676 # Mock response from Zotero API 1682 1677 mock_response = { ··· 1684 1679 "ITEM2": {"version": 456, "indexedChars": 5000, "totalChars": 5000}, 1685 1680 } 1686 1681 1687 - HTTPretty.register_uri( 1688 - HTTPretty.GET, 1682 + mock.register( 1683 + "GET", 1689 1684 "https://api.zotero.org/users/myuserID/fulltext", 1690 1685 body=json.dumps(mock_response), 1691 1686 content_type="application/json", ··· 1702 1697 self.assertEqual(result, mock_response) 1703 1698 1704 1699 # Check that the correct parameters were sent 1705 - request = httpretty.last_request() 1700 + request = mock.last_request() 1706 1701 self.assertEqual(request.querystring.get("since"), ["5"]) 1707 1702 1708 - @httpretty.activate 1709 1703 def test_last_modified_version(self): 1710 1704 """Test the last_modified_version method""" 1711 - zot = z.Zotero("myuserID", "user", "myuserkey") 1705 + mock = MockClient() 1706 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1712 1707 1713 1708 # Mock the response with a last-modified-version header 1714 - HTTPretty.register_uri( 1715 - HTTPretty.GET, 1709 + mock.register( 1710 + "GET", 1716 1711 "https://api.zotero.org/users/myuserID/items", 1717 1712 body=self.items_doc, 1718 1713 content_type="application/json", 1719 - adding_headers={"last-modified-version": "1234"}, 1714 + headers={"last-modified-version": "1234"}, 1720 1715 ) 1721 1716 1722 1717 # Test retrieving the last modified version ··· 1745 1740 result = zot.makeiter(lambda: None) 1746 1741 1747 1742 # Verify makeiter sets the 'next' link to the 'self' link 1743 + assert zot.links is not None 1748 1744 self.assertEqual(zot.links["next"], zot.links["self"]) 1749 1745 1750 1746 # Verify makeiter returns an iterable ··· 1770 1766 zot.makeiter(lambda: None) 1771 1767 1772 1768 # Verify that the 'next' link was set to 'self' and still contains limit parameter 1769 + assert zot.links is not None 1773 1770 self.assertEqual(zot.links["next"], test_self_link) 1774 1771 self.assertIn("limit=1", zot.links["next"]) 1775 1772 self.assertIn("locale=en-US", zot.links["next"]) 1776 1773 1777 - @httpretty.activate 1778 1774 def test_publications_user(self): 1779 1775 """Test the publications method for user libraries""" 1780 - zot = z.Zotero("myuserID", "user", "myuserkey") 1776 + mock = MockClient() 1777 + zot = z.Zotero("myuserID", "user", "myuserkey", client=mock.client) 1781 1778 1782 1779 # Mock the API response 1783 - HTTPretty.register_uri( 1784 - HTTPretty.GET, 1780 + mock.register( 1781 + "GET", 1785 1782 "https://api.zotero.org/users/myuserID/publications/items", 1786 1783 body=self.items_doc, 1787 1784 content_type="application/json", ··· 1813 1810 current_dt = whenever.ZonedDateTime.now("GMT").py_datetime() 1814 1811 1815 1812 # Assert that both produce GMT timezone 1816 - self.assertEqual(old_dt.tzinfo.zone, "GMT") 1813 + assert old_dt.tzinfo is not None 1814 + assert current_dt.tzinfo is not None 1815 + self.assertEqual(getattr(old_dt.tzinfo, "zone", None), "GMT") 1817 1816 self.assertEqual(current_dt.tzinfo.tzname(None), "GMT") 1818 1817 1819 1818 # Assert that timezone names are equivalent 1820 - self.assertEqual(old_dt.tzinfo.zone, current_dt.tzinfo.tzname(None)) 1819 + self.assertEqual( 1820 + getattr(old_dt.tzinfo, "zone", None), current_dt.tzinfo.tzname(None) 1821 + ) 1821 1822 1822 1823 def test_timezone_behavior_instant_vs_zoned(self): 1823 1824 """Test that ZonedDateTime produces correct GMT while Instant produces UTC"""
+15 -13
uv.lock
··· 2 2 revision = 3 3 3 requires-python = ">=3.9" 4 4 resolution-markers = [ 5 - "python_full_version >= '3.11'", 5 + "python_full_version >= '3.14'", 6 + "python_full_version == '3.13.*'", 7 + "python_full_version >= '3.11' and python_full_version < '3.13'", 6 8 "python_full_version == '3.10.*'", 7 9 "python_full_version < '3.10'", 8 10 ] ··· 24 26 version = "1.0.0" 25 27 source = { registry = "https://pypi.org/simple" } 26 28 resolution-markers = [ 27 - "python_full_version >= '3.11'", 29 + "python_full_version >= '3.14'", 30 + "python_full_version == '3.13.*'", 31 + "python_full_version >= '3.11' and python_full_version < '3.13'", 28 32 "python_full_version == '3.10.*'", 29 33 ] 30 34 sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } ··· 187 191 version = "8.3.0" 188 192 source = { registry = "https://pypi.org/simple" } 189 193 resolution-markers = [ 190 - "python_full_version >= '3.11'", 194 + "python_full_version >= '3.14'", 195 + "python_full_version == '3.13.*'", 196 + "python_full_version >= '3.11' and python_full_version < '3.13'", 191 197 "python_full_version == '3.10.*'", 192 198 ] 193 199 dependencies = [ ··· 378 384 ] 379 385 380 386 [[package]] 381 - name = "httpretty" 382 - version = "1.1.4" 383 - source = { registry = "https://pypi.org/simple" } 384 - sdist = { url = "https://files.pythonhosted.org/packages/6e/19/850b7ed736319d0c4088581f4fc34f707ef14461947284026664641e16d4/httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68", size = 442389, upload-time = "2021-08-16T19:35:31.4Z" } 385 - 386 - [[package]] 387 387 name = "httpx" 388 388 version = "0.28.1" 389 389 source = { registry = "https://pypi.org/simple" } ··· 492 492 version = "9.5.0" 493 493 source = { registry = "https://pypi.org/simple" } 494 494 resolution-markers = [ 495 - "python_full_version >= '3.11'", 495 + "python_full_version >= '3.14'", 496 + "python_full_version == '3.13.*'", 497 + "python_full_version >= '3.11' and python_full_version < '3.13'", 496 498 ] 497 499 dependencies = [ 498 500 { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, ··· 801 803 802 804 [package.dev-dependencies] 803 805 dev = [ 804 - { name = "httpretty" }, 805 806 { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 806 807 { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, 807 808 { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ··· 831 832 832 833 [package.metadata.requires-dev] 833 834 dev = [ 834 - { name = "httpretty", specifier = ">=1.1.4" }, 835 835 { name = "ipython" }, 836 836 { name = "pytest", specifier = ">=8.4.2" }, 837 837 { name = "pytest-asyncio" }, ··· 970 970 version = "8.2.3" 971 971 source = { registry = "https://pypi.org/simple" } 972 972 resolution-markers = [ 973 - "python_full_version >= '3.11'", 973 + "python_full_version >= '3.14'", 974 + "python_full_version == '3.13.*'", 975 + "python_full_version >= '3.11' and python_full_version < '3.13'", 974 976 ] 975 977 dependencies = [ 976 978 { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },