|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import asyncio |
| 4 | +import json |
4 | 5 | from logging import getLogger |
5 | 6 | from typing import TYPE_CHECKING |
6 | 7 |
|
7 | 8 | from typing_extensions import override |
8 | 9 |
|
9 | 10 | from crawlee._utils.byte_size import ByteSize |
10 | | -from crawlee._utils.file import json_dumps |
11 | 11 | from crawlee.storage_clients._base import DatasetClient |
12 | 12 | from crawlee.storage_clients.models import DatasetItemsListPage, DatasetMetadata |
13 | 13 |
|
@@ -138,18 +138,16 @@ async def drop(self) -> None: |
138 | 138 |
|
139 | 139 | @override |
140 | 140 | async def push_data(self, data: Sequence[Mapping[str, JsonSerializable]] | Mapping[str, JsonSerializable]) -> None: |
141 | | - async def payloads_generator(items: Sequence[Mapping[str, JsonSerializable]]) -> AsyncIterator[str]: |
142 | | - for index, item in enumerate(items): |
143 | | - yield await self._check_and_serialize(item, index) |
144 | | - |
145 | 141 | async with self._charge_lock(), self._lock: |
146 | 142 | items = data if self._is_sequence_of_items(data) else [data] |
147 | 143 | if not items: |
148 | 144 | return |
149 | 145 | limit = self._compute_limit_for_push(len(items)) |
150 | 146 | items = items[:limit] |
151 | 147 |
|
152 | | - async for chunk in self._chunk_by_size(payloads_generator(items)): |
| 148 | + offset = 0 |
| 149 | + while offset < len(items): |
| 150 | + chunk, offset = await asyncio.to_thread(self._serialize_chunk, items, offset) |
153 | 151 | await self._api_client.push_items(items=chunk) |
154 | 152 |
|
155 | 153 | await self._charge_for_items(count_items=limit) |
@@ -213,58 +211,43 @@ async def iterate_items( |
213 | 211 | yield item |
214 | 212 |
|
215 | 213 | @classmethod |
216 | | - async def _check_and_serialize(cls, item: Mapping[str, JsonSerializable], index: int | None = None) -> str: |
217 | | - """Serialize a given item to JSON, checks its serializability and size against a limit. |
| 214 | + def _serialize_chunk(cls, items: Sequence[Mapping[str, JsonSerializable]], offset: int) -> tuple[str, int]: |
| 215 | + """Serialize items starting at `offset` into one JSON array staying within the payload size limit. |
| 216 | +
|
| 217 | + The array holds as many consecutive items as fit below `_EFFECTIVE_LIMIT_SIZE`, always at least one. |
| 218 | + The result is compact JSON - it goes on the wire, so indentation and separator padding would be |
| 219 | + pure overhead. This is CPU-bound and blocking; call it via `asyncio.to_thread`. |
218 | 220 |
|
219 | 221 | Args: |
220 | | - item: The item to serialize. |
221 | | - index: Index of the item, used for error context. |
| 222 | + items: The items to serialize. |
| 223 | + offset: Index of the first item to serialize. |
222 | 224 |
|
223 | 225 | Returns: |
224 | | - Serialized JSON string. |
| 226 | + The JSON array string and the index of the first item that did not fit into it. |
225 | 227 |
|
226 | 228 | Raises: |
227 | | - ValueError: If item is not JSON serializable or exceeds size limit. |
| 229 | + ValueError: If an item is not JSON serializable or on its own exceeds the size limit. |
228 | 230 | """ |
229 | | - s = ' ' if index is None else f' at index {index} ' |
230 | | - |
231 | | - try: |
232 | | - payload = await json_dumps(item) |
233 | | - except Exception as exc: |
234 | | - raise ValueError(f'Data item{s}is not serializable to JSON.') from exc |
235 | | - |
236 | | - payload_size = ByteSize(len(payload.encode('utf-8'))) |
237 | | - if payload_size > cls._EFFECTIVE_LIMIT_SIZE: |
238 | | - raise ValueError(f'Data item{s}is too large (size: {payload_size}, limit: {cls._EFFECTIVE_LIMIT_SIZE})') |
239 | | - |
240 | | - return payload |
241 | | - |
242 | | - async def _chunk_by_size(self, items: AsyncIterator[str]) -> AsyncIterator[str]: |
243 | | - """Yield chunks of JSON arrays composed of input strings, respecting a size limit. |
| 231 | + payloads: list[str] = [] |
| 232 | + chunk_size = ByteSize(2) # Add 2 bytes for [] wrapper. |
244 | 233 |
|
245 | | - Groups an iterable of JSON string payloads into larger JSON arrays, ensuring the total size |
246 | | - of each array does not exceed `EFFECTIVE_LIMIT_SIZE`. Each output is a JSON array string that |
247 | | - contains as many payloads as possible without breaching the size threshold, maintaining the |
248 | | - order of the original payloads. Assumes individual items are below the size limit. |
| 234 | + for index in range(offset, len(items)): |
| 235 | + try: |
| 236 | + payload = json.dumps(items[index], ensure_ascii=False, separators=(',', ':'), default=str) |
| 237 | + except Exception as exc: |
| 238 | + raise ValueError(f'Data item at index {index} is not serializable to JSON.') from exc |
249 | 239 |
|
250 | | - Args: |
251 | | - items: Iterable of JSON string payloads. |
252 | | -
|
253 | | - Yields: |
254 | | - Strings representing JSON arrays of payloads, each staying within the size limit. |
255 | | - """ |
256 | | - last_chunk_size = ByteSize(2) # Add 2 bytes for [] wrapper. |
257 | | - current_chunk = [] |
258 | | - |
259 | | - async for payload in items: |
260 | 240 | payload_size = ByteSize(len(payload.encode('utf-8'))) |
| 241 | + if payload_size > cls._EFFECTIVE_LIMIT_SIZE: |
| 242 | + raise ValueError( |
| 243 | + f'Data item at index {index} is too large ' |
| 244 | + f'(size: {payload_size}, limit: {cls._EFFECTIVE_LIMIT_SIZE})' |
| 245 | + ) |
| 246 | + |
| 247 | + if payloads and chunk_size + payload_size > cls._EFFECTIVE_LIMIT_SIZE: |
| 248 | + return f'[{",".join(payloads)}]', index |
261 | 249 |
|
262 | | - if last_chunk_size + payload_size <= self._EFFECTIVE_LIMIT_SIZE: |
263 | | - current_chunk.append(payload) |
264 | | - last_chunk_size += payload_size + ByteSize(1) # Add 1 byte for ',' separator. |
265 | | - else: |
266 | | - yield f'[{",".join(current_chunk)}]' |
267 | | - current_chunk = [payload] |
268 | | - last_chunk_size = payload_size + ByteSize(2) # Add 2 bytes for [] wrapper. |
| 250 | + payloads.append(payload) |
| 251 | + chunk_size += payload_size + ByteSize(1) # Add 1 byte for ',' separator. |
269 | 252 |
|
270 | | - yield f'[{",".join(current_chunk)}]' |
| 253 | + return f'[{",".join(payloads)}]', len(items) |
0 commit comments