internetdata 1.0.0__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.
Files changed (46) hide show
  1. internetdata/__init__.py +49 -0
  2. internetdata/_core.py +287 -0
  3. internetdata/_generated/__init__.py +8 -0
  4. internetdata/_generated/api/__init__.py +1 -0
  5. internetdata/_generated/api/database_v_2/__init__.py +1 -0
  6. internetdata/_generated/api/database_v_2/database_checksum_v2.py +203 -0
  7. internetdata/_generated/api/database_v_2/database_metadata_v2.py +205 -0
  8. internetdata/_generated/api/database_v_2/download_database_v2.py +213 -0
  9. internetdata/_generated/api/database_v_2/list_databases.py +162 -0
  10. internetdata/_generated/api/database_v_2/list_downloads.py +178 -0
  11. internetdata/_generated/client.py +272 -0
  12. internetdata/_generated/errors.py +16 -0
  13. internetdata/_generated/models/__init__.py +53 -0
  14. internetdata/_generated/models/database.py +233 -0
  15. internetdata/_generated/models/database_checksum_v2_format.py +9 -0
  16. internetdata/_generated/models/database_checksum_v2_response_200.py +85 -0
  17. internetdata/_generated/models/database_checksum_v2_response_200_format.py +9 -0
  18. internetdata/_generated/models/database_metadata.py +132 -0
  19. internetdata/_generated/models/database_metadata_column.py +80 -0
  20. internetdata/_generated/models/database_metadata_sample.py +78 -0
  21. internetdata/_generated/models/database_metadata_sample_additional_property_item.py +45 -0
  22. internetdata/_generated/models/database_metadata_schema.py +72 -0
  23. internetdata/_generated/models/database_metadata_size.py +47 -0
  24. internetdata/_generated/models/database_redistribution_type_1.py +10 -0
  25. internetdata/_generated/models/database_redistribution_type_2_type_1.py +10 -0
  26. internetdata/_generated/models/database_redistribution_type_3_type_1.py +10 -0
  27. internetdata/_generated/models/database_standing.py +10 -0
  28. internetdata/_generated/models/database_version.py +97 -0
  29. internetdata/_generated/models/database_version_formats_item.py +9 -0
  30. internetdata/_generated/models/db_checksums.py +85 -0
  31. internetdata/_generated/models/download.py +161 -0
  32. internetdata/_generated/models/download_database_v2_format.py +9 -0
  33. internetdata/_generated/models/download_outcome.py +13 -0
  34. internetdata/_generated/models/error.py +62 -0
  35. internetdata/_generated/models/list_databases_response_200.py +75 -0
  36. internetdata/_generated/models/list_downloads_response_200.py +75 -0
  37. internetdata/_generated/types.py +54 -0
  38. internetdata/aio.py +268 -0
  39. internetdata/client.py +278 -0
  40. internetdata/errors.py +128 -0
  41. internetdata/models.py +197 -0
  42. internetdata/py.typed +0 -0
  43. internetdata-1.0.0.dist-info/METADATA +184 -0
  44. internetdata-1.0.0.dist-info/RECORD +46 -0
  45. internetdata-1.0.0.dist-info/WHEEL +4 -0
  46. internetdata-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import TYPE_CHECKING, Any, Self, TypeVar
5
+
6
+ from attrs import define as _attrs_define
7
+ from attrs import field as _attrs_field
8
+
9
+ if TYPE_CHECKING:
10
+ from ..models.download import Download
11
+
12
+
13
+ T = TypeVar("T", bound="ListDownloadsResponse200")
14
+
15
+
16
+ @_attrs_define
17
+ class ListDownloadsResponse200:
18
+ """
19
+ Attributes:
20
+ downloads (list[Download]):
21
+ """
22
+
23
+ downloads: list[Download]
24
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ downloads = []
28
+ for downloads_item_data in self.downloads:
29
+ downloads_item = downloads_item_data.to_dict()
30
+ downloads.append(downloads_item)
31
+
32
+ field_dict: dict[str, Any] = {}
33
+ field_dict.update(self.additional_properties)
34
+ field_dict.update(
35
+ {
36
+ "downloads": downloads,
37
+ }
38
+ )
39
+
40
+ return field_dict
41
+
42
+ @classmethod
43
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
44
+ from ..models.download import Download
45
+
46
+ d = dict(src_dict)
47
+ downloads = []
48
+ _downloads = d.pop("downloads")
49
+ for downloads_item_data in _downloads:
50
+ downloads_item = Download.from_dict(downloads_item_data)
51
+
52
+ downloads.append(downloads_item)
53
+
54
+ list_downloads_response_200 = cls(
55
+ downloads=downloads,
56
+ )
57
+
58
+ list_downloads_response_200.additional_properties = d
59
+ return list_downloads_response_200
60
+
61
+ @property
62
+ def additional_keys(self) -> list[str]:
63
+ return list(self.additional_properties.keys())
64
+
65
+ def __getitem__(self, key: str) -> Any:
66
+ return self.additional_properties[key]
67
+
68
+ def __setitem__(self, key: str, value: Any) -> None:
69
+ self.additional_properties[key] = value
70
+
71
+ def __delitem__(self, key: str) -> None:
72
+ del self.additional_properties[key]
73
+
74
+ def __contains__(self, key: str) -> bool:
75
+ return key in self.additional_properties
@@ -0,0 +1,54 @@
1
+ """Contains some shared types for properties"""
2
+
3
+ from collections.abc import Mapping, MutableMapping
4
+ from http import HTTPStatus
5
+ from typing import IO, BinaryIO, Generic, Literal, TypeVar
6
+
7
+ from attrs import define
8
+
9
+
10
+ class Unset:
11
+ def __bool__(self) -> Literal[False]:
12
+ return False
13
+
14
+
15
+ UNSET: Unset = Unset()
16
+
17
+ # The types that `httpx.Client(files=)` can accept, copied from that library.
18
+ FileContent = IO[bytes] | bytes | str
19
+ FileTypes = (
20
+ # (filename, file (or bytes), content_type)
21
+ tuple[str | None, FileContent, str | None]
22
+ # (filename, file (or bytes), content_type, headers)
23
+ | tuple[str | None, FileContent, str | None, Mapping[str, str]]
24
+ )
25
+ RequestFiles = list[tuple[str, FileTypes]]
26
+
27
+
28
+ @define
29
+ class File:
30
+ """Contains information for file uploads"""
31
+
32
+ payload: BinaryIO
33
+ file_name: str | None = None
34
+ mime_type: str | None = None
35
+
36
+ def to_tuple(self) -> FileTypes:
37
+ """Return a tuple representation that httpx will accept for multipart/form-data"""
38
+ return self.file_name, self.payload, self.mime_type
39
+
40
+
41
+ T = TypeVar("T")
42
+
43
+
44
+ @define
45
+ class Response(Generic[T]):
46
+ """A response from an endpoint"""
47
+
48
+ status_code: HTTPStatus
49
+ content: bytes
50
+ headers: MutableMapping[str, str]
51
+ parsed: T | None
52
+
53
+
54
+ __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
internetdata/aio.py ADDED
@@ -0,0 +1,268 @@
1
+ """The asyncio client.
2
+
3
+ Mirrors `client.py` method for method; only the waiting differs. The two are written out
4
+ rather than shared because every difference between them is an `await`, and the wrappers
5
+ that hide that are harder to read than the duplication.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import builtins
12
+ import os
13
+ from collections.abc import Awaitable, Callable
14
+ from types import TracebackType
15
+ from typing import Self, TypeVar
16
+
17
+ import httpx
18
+
19
+ from ._core import (
20
+ DEFAULT_BASE_URL,
21
+ DEFAULT_DOWNLOADS_LIMIT,
22
+ DEFAULT_RETRIES,
23
+ DEFAULT_TIMEOUT,
24
+ TRANSFER_CHUNK_BYTES,
25
+ as_error,
26
+ assert_whole_transfer,
27
+ build_async_transfer_client,
28
+ build_client,
29
+ checksums_of,
30
+ databases_of,
31
+ downloads_of,
32
+ parse_body,
33
+ part_file,
34
+ redirect_location,
35
+ retry_delay,
36
+ send_async,
37
+ storage_refusal,
38
+ unwrap,
39
+ )
40
+ from ._generated.api.database_v_2 import (
41
+ database_checksum_v2,
42
+ database_metadata_v2,
43
+ download_database_v2,
44
+ list_databases,
45
+ list_downloads,
46
+ )
47
+ from ._generated.client import AuthenticatedClient
48
+ from ._generated.models.database_checksum_v2_format import DatabaseChecksumV2Format
49
+ from ._generated.models.download_database_v2_format import DownloadDatabaseV2Format
50
+ from .errors import InternetDataError
51
+ from .models import Database, DatabaseMetadata, Download, Format, to_metadata
52
+
53
+ __all__ = ["AsyncDatabaseApi", "AsyncInternetData"]
54
+
55
+ T = TypeVar("T")
56
+
57
+
58
+ class AsyncInternetData:
59
+ """A client for the InternetData API, for asyncio.
60
+
61
+ Identical in behavior to `InternetData`. Close it with `await client.aclose()`, or
62
+ use it as an async context manager.
63
+ """
64
+
65
+ database: AsyncDatabaseApi
66
+ """The licensed database catalog and downloads, which is the whole API."""
67
+
68
+ def __init__(
69
+ self,
70
+ api_key: str | None = None,
71
+ *,
72
+ base_url: str = DEFAULT_BASE_URL,
73
+ retries: int = DEFAULT_RETRIES,
74
+ timeout: float | None = DEFAULT_TIMEOUT,
75
+ transport: httpx.AsyncBaseTransport | None = None,
76
+ ) -> None:
77
+ self._client = build_client(api_key, base_url, timeout, transport)
78
+ self._transfer = build_async_transfer_client(timeout, transport)
79
+ self._retries = retries
80
+ self.database = AsyncDatabaseApi(self)
81
+
82
+ async def aclose(self) -> None:
83
+ await self._client.get_async_httpx_client().aclose()
84
+ await self._transfer.aclose()
85
+
86
+ async def __aenter__(self) -> Self:
87
+ return self
88
+
89
+ async def __aexit__(
90
+ self,
91
+ exc_type: type[BaseException] | None,
92
+ exc: BaseException | None,
93
+ tb: TracebackType | None,
94
+ ) -> None:
95
+ await self.aclose()
96
+
97
+ async def _retrying(self, call: Callable[[], Awaitable[T]], retries: int) -> T:
98
+ attempt = 0
99
+ while True:
100
+ try:
101
+ return await call()
102
+ except (InternetDataError, httpx.HTTPError) as exc:
103
+ err = as_error(exc)
104
+ delay = retry_delay(err, attempt, retries)
105
+ if delay is None:
106
+ raise err
107
+ await asyncio.sleep(delay)
108
+ attempt += 1
109
+
110
+
111
+ class AsyncDatabaseApi:
112
+ """The database catalog and downloads. Access is granted by contract, not self-serve.
113
+
114
+ `list` is a method here, which shadows the builtin for everything else in the class
115
+ body, so the return annotations name `builtins.list` explicitly.
116
+ """
117
+
118
+ def __init__(self, owner: AsyncInternetData) -> None:
119
+ self._owner = owner
120
+
121
+ async def list(self) -> builtins.list[Database]:
122
+ """The published catalog as YOUR organization may see it.
123
+
124
+ Every family carries a `standing`, so one you have never bought is listed as
125
+ `unlicensed` rather than hidden. The exception is a family commissioned for a
126
+ single customer, which is absent entirely for everyone else. Nothing is cached
127
+ and nothing is reconstructed here - what you get is what the server sent for the
128
+ key you are holding.
129
+ """
130
+
131
+ async def call() -> builtins.list[Database]:
132
+ res = await send_async(lambda: list_databases.asyncio_detailed(client=self._client))
133
+ return parse_body(unwrap(res), databases_of)
134
+
135
+ return await self._retrying(call)
136
+
137
+ async def metadata(self, database_id: str) -> DatabaseMetadata:
138
+ """What is inside one database: freshness, row count, columns, samples and sizes."""
139
+
140
+ async def call() -> DatabaseMetadata:
141
+ res = await send_async(
142
+ lambda: database_metadata_v2.asyncio_detailed(client=self._client, id=database_id)
143
+ )
144
+ return parse_body(unwrap(res), to_metadata)
145
+
146
+ return await self._retrying(call)
147
+
148
+ async def checksums(self, database_id: str, format: Format) -> dict[str, str]:
149
+ """Every checksum published for one database file, keyed by algorithm."""
150
+
151
+ async def call() -> dict[str, str]:
152
+ res = await send_async(
153
+ lambda: database_checksum_v2.asyncio_detailed(
154
+ client=self._client,
155
+ id=database_id,
156
+ format_=DatabaseChecksumV2Format(format),
157
+ )
158
+ )
159
+ return parse_body(unwrap(res), checksums_of)
160
+
161
+ return await self._retrying(call)
162
+
163
+ async def downloads(self, limit: int = DEFAULT_DOWNLOADS_LIMIT) -> builtins.list[Download]:
164
+ """Your organization's recent download attempts, newest first."""
165
+
166
+ async def call() -> builtins.list[Download]:
167
+ res = await send_async(
168
+ lambda: list_downloads.asyncio_detailed(client=self._client, limit=limit)
169
+ )
170
+ return parse_body(unwrap(res), downloads_of)
171
+
172
+ return await self._retrying(call)
173
+
174
+ async def download_url(self, database_id: str, format: Format) -> str:
175
+ """The time-limited URL for one database file.
176
+
177
+ The API answers `302` to object storage, and the link carries its own signature,
178
+ so it holds no API key and can be handed to anything that speaks HTTP. Returned
179
+ rather than followed so the caller decides how to move a file that reaches
180
+ gigabytes; the link authorizes the START of a transfer, so one already running is
181
+ not interrupted when it lapses.
182
+ """
183
+
184
+ async def call() -> str:
185
+ res = await send_async(
186
+ lambda: download_database_v2.asyncio_detailed(
187
+ client=self._client,
188
+ id=database_id,
189
+ format_=DownloadDatabaseV2Format(format),
190
+ )
191
+ )
192
+ return redirect_location(res)
193
+
194
+ return await self._retrying(call)
195
+
196
+ async def download(self, database_id: str, format: Format, path: str | os.PathLike[str]) -> int:
197
+ """Download one database file to `path`, and return the bytes written.
198
+
199
+ The bytes are streamed straight to disk, so nothing larger than a chunk is ever
200
+ held in memory whatever the database weighs. They land in a neighboring `.part`
201
+ file that is moved into place only once the whole transfer has arrived, so a
202
+ failure leaves neither a truncated file at `path` nor the `.part` behind, and an
203
+ existing copy at `path` survives a refresh that fails.
204
+
205
+ A failure DURING the transfer surfaces as it happened, an `httpx` error or an
206
+ `OSError`, rather than as this library's error type: a reset socket and a full
207
+ disk are different problems, and only one of them is ours.
208
+
209
+ Each chunk is written from a worker thread. A gigabyte of blocking writes on the
210
+ event loop would stall every other task in the process for the length of the
211
+ transfer, which is the one thing an async caller cannot afford.
212
+ """
213
+ res = await self._open_transfer(database_id, format)
214
+ try:
215
+ loop = asyncio.get_running_loop()
216
+ written = 0
217
+ with part_file(path) as sink:
218
+ async for chunk in res.aiter_bytes(TRANSFER_CHUNK_BYTES):
219
+ await loop.run_in_executor(None, sink.write, chunk)
220
+ written += len(chunk)
221
+ # Inside, so a short transfer fails before anything is moved into place.
222
+ assert_whole_transfer(res, written)
223
+ return written
224
+ finally:
225
+ await res.aclose()
226
+
227
+ async def download_bytes(self, database_id: str, format: Format) -> bytes:
228
+ """Download one database file and hand back its bytes.
229
+
230
+ **This holds the entire file in memory**, and the catalog spans seven orders of
231
+ magnitude, from a few hundred bytes to several gigabytes. Reach for it at the
232
+ small end, where the bytes go straight into a parser, and use `download` for
233
+ anything you have not checked `metadata` for.
234
+ """
235
+ res = await self._open_transfer(database_id, format)
236
+ try:
237
+ body = await res.aread()
238
+ assert_whole_transfer(res, len(body))
239
+ return body
240
+ finally:
241
+ await res.aclose()
242
+
243
+ # Follows the 302 as a SECOND, unauthenticated request rather than by loosening the
244
+ # redirect guard: the presigned URL authorizes itself, so forwarding the API key
245
+ # would hand a credential to a host with no business holding it - and object storage
246
+ # rejects a request carrying both signatures outright.
247
+ #
248
+ # Returns the response with its body still unread, so the caller decides whether a
249
+ # database is going to disk or into memory.
250
+ async def _open_transfer(self, database_id: str, format: Format) -> httpx.Response:
251
+ url = await self.download_url(database_id, format)
252
+ transfer = self._owner._transfer
253
+
254
+ async def call() -> httpx.Response:
255
+ res = await transfer.send(transfer.build_request("GET", url), stream=True)
256
+ if res.status_code != httpx.codes.OK:
257
+ await res.aclose()
258
+ raise storage_refusal(res)
259
+ return res
260
+
261
+ return await self._retrying(call)
262
+
263
+ @property
264
+ def _client(self) -> AuthenticatedClient:
265
+ return self._owner._client
266
+
267
+ async def _retrying(self, call: Callable[[], Awaitable[T]]) -> T:
268
+ return await self._owner._retrying(call, self._owner._retries)
internetdata/client.py ADDED
@@ -0,0 +1,278 @@
1
+ """The synchronous client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ import os
7
+ import time
8
+ from collections.abc import Callable
9
+ from types import TracebackType
10
+ from typing import Self, TypeVar
11
+
12
+ import httpx
13
+
14
+ from ._core import (
15
+ DEFAULT_BASE_URL,
16
+ DEFAULT_DOWNLOADS_LIMIT,
17
+ DEFAULT_RETRIES,
18
+ DEFAULT_TIMEOUT,
19
+ TRANSFER_CHUNK_BYTES,
20
+ as_error,
21
+ assert_whole_transfer,
22
+ build_client,
23
+ build_transfer_client,
24
+ checksums_of,
25
+ databases_of,
26
+ downloads_of,
27
+ parse_body,
28
+ part_file,
29
+ redirect_location,
30
+ retry_delay,
31
+ send,
32
+ storage_refusal,
33
+ unwrap,
34
+ )
35
+ from ._generated.api.database_v_2 import (
36
+ database_checksum_v2,
37
+ database_metadata_v2,
38
+ download_database_v2,
39
+ list_databases,
40
+ list_downloads,
41
+ )
42
+ from ._generated.client import AuthenticatedClient
43
+ from ._generated.models.database_checksum_v2_format import DatabaseChecksumV2Format
44
+ from ._generated.models.download_database_v2_format import DownloadDatabaseV2Format
45
+ from .errors import InternetDataError
46
+ from .models import Database, DatabaseMetadata, Download, Format, to_metadata
47
+
48
+ __all__ = ["DatabaseApi", "InternetData"]
49
+
50
+ T = TypeVar("T")
51
+
52
+
53
+ class InternetData:
54
+ """A client for the InternetData API.
55
+
56
+ Every database published today is licensed, so create a key carrying the
57
+ `db.download` scope in the console and pass it in. The argument is optional
58
+ nonetheless, and an absent or empty one sends no `Authorization` header at all
59
+ rather than an empty one: what this API serves without a licence is a product
60
+ decision, not the client's to refuse.
61
+
62
+ Holds an HTTP connection pool, so use it as a context manager or call `close()` when
63
+ you are done with it.
64
+ """
65
+
66
+ database: DatabaseApi
67
+ """The licensed database catalog and downloads, which is the whole API."""
68
+
69
+ def __init__(
70
+ self,
71
+ api_key: str | None = None,
72
+ *,
73
+ base_url: str = DEFAULT_BASE_URL,
74
+ retries: int = DEFAULT_RETRIES,
75
+ timeout: float | None = DEFAULT_TIMEOUT,
76
+ transport: httpx.BaseTransport | None = None,
77
+ ) -> None:
78
+ self._client = build_client(api_key, base_url, timeout, transport)
79
+ self._transfer = build_transfer_client(timeout, transport)
80
+ self._retries = retries
81
+ self.database = DatabaseApi(self)
82
+
83
+ def close(self) -> None:
84
+ self._client.get_httpx_client().close()
85
+ self._transfer.close()
86
+
87
+ def __enter__(self) -> Self:
88
+ return self
89
+
90
+ def __exit__(
91
+ self,
92
+ exc_type: type[BaseException] | None,
93
+ exc: BaseException | None,
94
+ tb: TracebackType | None,
95
+ ) -> None:
96
+ self.close()
97
+
98
+ def _retrying(self, call: Callable[[], T], retries: int) -> T:
99
+ attempt = 0
100
+ while True:
101
+ try:
102
+ return call()
103
+ except (InternetDataError, httpx.HTTPError) as exc:
104
+ err = as_error(exc)
105
+ delay = retry_delay(err, attempt, retries)
106
+ if delay is None:
107
+ raise err
108
+ time.sleep(delay)
109
+ attempt += 1
110
+
111
+
112
+ class DatabaseApi:
113
+ """The database catalog and downloads. Access is granted by contract, not self-serve.
114
+
115
+ `list` is a method here, which shadows the builtin for everything else in the class
116
+ body, so the return annotations name `builtins.list` explicitly.
117
+ """
118
+
119
+ def __init__(self, owner: InternetData) -> None:
120
+ self._owner = owner
121
+
122
+ def list(self) -> builtins.list[Database]:
123
+ """The published catalog as YOUR organization may see it.
124
+
125
+ Every family carries a `standing`, so one you have never bought is listed as
126
+ `unlicensed` rather than hidden: the catalog is a shop window as much as an
127
+ inventory. The exception is a family commissioned for a single customer, which is
128
+ absent entirely for everyone else, because listing it would advertise that
129
+ customer. Nothing is cached and nothing is reconstructed here - what you get is
130
+ what the server sent for the key you are holding.
131
+
132
+ A licence covers a FAMILY, while a download names a version, so the ids for the
133
+ other calls come from each entry's `versions`.
134
+ """
135
+
136
+ def call() -> builtins.list[Database]:
137
+ res = send(lambda: list_databases.sync_detailed(client=self._client))
138
+ return parse_body(unwrap(res), databases_of)
139
+
140
+ return self._retrying(call)
141
+
142
+ def metadata(self, database_id: str) -> DatabaseMetadata:
143
+ """What is inside one database: freshness, row count, columns, samples and sizes.
144
+
145
+ Cheap enough to poll: it answers `updated` and `entries` without moving the
146
+ build. `size` is the number to budget a transfer against before starting one.
147
+ """
148
+
149
+ def call() -> DatabaseMetadata:
150
+ res = send(
151
+ lambda: database_metadata_v2.sync_detailed(client=self._client, id=database_id)
152
+ )
153
+ return parse_body(unwrap(res), to_metadata)
154
+
155
+ return self._retrying(call)
156
+
157
+ def checksums(self, database_id: str, format: Format) -> dict[str, str]:
158
+ """Every checksum published for one database file, keyed by algorithm.
159
+
160
+ Keyed rather than one digest because the API publishes md5, sha1, sha256 and
161
+ sha512 side by side and which of them you want is your verifier's business.
162
+ """
163
+
164
+ def call() -> dict[str, str]:
165
+ res = send(
166
+ lambda: database_checksum_v2.sync_detailed(
167
+ client=self._client,
168
+ id=database_id,
169
+ format_=DatabaseChecksumV2Format(format),
170
+ )
171
+ )
172
+ return parse_body(unwrap(res), checksums_of)
173
+
174
+ return self._retrying(call)
175
+
176
+ def downloads(self, limit: int = DEFAULT_DOWNLOADS_LIMIT) -> builtins.list[Download]:
177
+ """Your organization's recent download attempts, newest first.
178
+
179
+ Refusals are listed too: a denial is what answers "it stopped working", and its
180
+ absence answers nothing.
181
+ """
182
+
183
+ def call() -> builtins.list[Download]:
184
+ res = send(lambda: list_downloads.sync_detailed(client=self._client, limit=limit))
185
+ return parse_body(unwrap(res), downloads_of)
186
+
187
+ return self._retrying(call)
188
+
189
+ def download_url(self, database_id: str, format: Format) -> str:
190
+ """The time-limited URL for one database file.
191
+
192
+ The API answers `302` to object storage, and the link carries its own signature,
193
+ so it holds no API key and can be handed to anything that speaks HTTP. Returned
194
+ rather than followed so the caller decides how to move a file that reaches
195
+ gigabytes; the link authorizes the START of a transfer, so one already running is
196
+ not interrupted when it lapses.
197
+ """
198
+
199
+ def call() -> str:
200
+ res = send(
201
+ lambda: download_database_v2.sync_detailed(
202
+ client=self._client,
203
+ id=database_id,
204
+ format_=DownloadDatabaseV2Format(format),
205
+ )
206
+ )
207
+ return redirect_location(res)
208
+
209
+ return self._retrying(call)
210
+
211
+ def download(self, database_id: str, format: Format, path: str | os.PathLike[str]) -> int:
212
+ """Download one database file to `path`, and return the bytes written.
213
+
214
+ The bytes are streamed straight to disk, so nothing larger than a chunk is ever
215
+ held in memory whatever the database weighs. They land in a neighboring `.part`
216
+ file that is moved into place only once the whole transfer has arrived, so a
217
+ failure leaves neither a truncated file at `path` nor the `.part` behind, and an
218
+ existing copy at `path` survives a refresh that fails.
219
+
220
+ A failure DURING the transfer surfaces as it happened, an `httpx` error or an
221
+ `OSError`, rather than as this library's error type: a reset socket and a full
222
+ disk are different problems, and only one of them is ours.
223
+ """
224
+ res = self._open_transfer(database_id, format)
225
+ try:
226
+ written = 0
227
+ with part_file(path) as sink:
228
+ for chunk in res.iter_bytes(TRANSFER_CHUNK_BYTES):
229
+ sink.write(chunk)
230
+ written += len(chunk)
231
+ # Inside, so a short transfer fails before anything is moved into place.
232
+ assert_whole_transfer(res, written)
233
+ return written
234
+ finally:
235
+ res.close()
236
+
237
+ def download_bytes(self, database_id: str, format: Format) -> bytes:
238
+ """Download one database file and hand back its bytes.
239
+
240
+ **This holds the entire file in memory**, and the catalog spans seven orders of
241
+ magnitude, from a few hundred bytes to several gigabytes. Reach for it at the
242
+ small end, where the bytes go straight into a parser, and use `download` for
243
+ anything you have not checked `metadata` for.
244
+ """
245
+ res = self._open_transfer(database_id, format)
246
+ try:
247
+ body = res.read()
248
+ assert_whole_transfer(res, len(body))
249
+ return body
250
+ finally:
251
+ res.close()
252
+
253
+ # Follows the 302 as a SECOND, unauthenticated request rather than by loosening the
254
+ # redirect guard: the presigned URL authorizes itself, so forwarding the API key
255
+ # would hand a credential to a host with no business holding it - and object storage
256
+ # rejects a request carrying both signatures outright.
257
+ #
258
+ # Returns the response with its body still unread, so the caller decides whether a
259
+ # database is going to disk or into memory.
260
+ def _open_transfer(self, database_id: str, format: Format) -> httpx.Response:
261
+ url = self.download_url(database_id, format)
262
+ transfer = self._owner._transfer
263
+
264
+ def call() -> httpx.Response:
265
+ res = transfer.send(transfer.build_request("GET", url), stream=True)
266
+ if res.status_code != httpx.codes.OK:
267
+ res.close()
268
+ raise storage_refusal(res)
269
+ return res
270
+
271
+ return self._retrying(call)
272
+
273
+ @property
274
+ def _client(self) -> AuthenticatedClient:
275
+ return self._owner._client
276
+
277
+ def _retrying(self, call: Callable[[], T]) -> T:
278
+ return self._owner._retrying(call, self._owner._retries)