scrapely-python 0.1.32__tar.gz → 0.1.34__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/CHANGELOG.md +8 -0
  2. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/PKG-INFO +1 -1
  3. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/pyproject.toml +1 -1
  4. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/__init__.py +1 -1
  5. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/actor.py +80 -5
  6. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely_python.egg-info/PKG-INFO +1 -1
  7. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/LICENSE +0 -0
  8. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/LICENSE-APACHE +0 -0
  9. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/MANIFEST.in +0 -0
  10. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/NOTICE +0 -0
  11. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/README.md +0 -0
  12. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/setup.cfg +0 -0
  13. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/_browser_locations.py +0 -0
  14. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/browser_configuration.py +0 -0
  15. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/cdp_retry.py +0 -0
  16. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/charging.py +0 -0
  17. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/charging_client.py +0 -0
  18. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/configuration.py +0 -0
  19. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/crypto/__init__.py +0 -0
  20. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/crypto/_secrets.py +0 -0
  21. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/errors.py +0 -0
  22. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/events/__init__.py +0 -0
  23. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/events/_types.py +0 -0
  24. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/events/event_manager.py +0 -0
  25. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/log.py +0 -0
  26. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/patch_browser_pool.py +0 -0
  27. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/pricing_types.py +0 -0
  28. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/proxy_configuration.py +0 -0
  29. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/py.typed +0 -0
  30. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/request_loaders/__init__.py +0 -0
  31. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
  32. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/__init__.py +0 -0
  33. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/_actor_runner.py +0 -0
  34. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/_async_thread.py +0 -0
  35. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/_logging_config.py +0 -0
  36. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/_serialization.py +0 -0
  37. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/_warnings.py +0 -0
  38. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/extensions/__init__.py +0 -0
  39. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/extensions/_graceful_stop.py +0 -0
  40. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/extensions/_httpcache.py +0 -0
  41. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/middlewares/__init__.py +0 -0
  42. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/middlewares/apify_proxy.py +0 -0
  43. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/pipelines/__init__.py +0 -0
  44. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/pipelines/actor_dataset_push.py +0 -0
  45. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/requests.py +0 -0
  46. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/scheduler.py +0 -0
  47. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/scrapy/utils.py +0 -0
  48. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/solver.py +0 -0
  49. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/__init__.py +0 -0
  50. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/_file_system/__init__.py +0 -0
  51. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
  52. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
  53. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/_metadata.py +0 -0
  54. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/api_client.py +0 -0
  55. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/client.py +0 -0
  56. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/patch_storage.py +0 -0
  57. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
  58. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
  59. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/scrapely_rq_client.py +0 -0
  60. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/scrapely_storage_client.py +0 -0
  61. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
  62. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/types.py +0 -0
  63. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage/utils.py +0 -0
  64. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storage_clients/__init__.py +0 -0
  65. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/storages/__init__.py +0 -0
  66. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/types.py +0 -0
  67. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely/webhook.py +0 -0
  68. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
  69. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
  70. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely_python.egg-info/requires.txt +0 -0
  71. {scrapely_python-0.1.32 → scrapely_python-0.1.34}/src/scrapely_python.egg-info/top_level.txt +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.34] - 2026-10-23
6
+
7
+ - Debug captures (`Actor.debug_capture`, `Actor.debug_capture_sync`) store parsed XML/HTML elements as their markup instead of a repr such as `<Element 'change' at 0x...>`: `xml.etree.ElementTree` elements and trees, lxml elements and trees, BeautifulSoup tags, selectolax nodes and parsel / Scrapy selectors, passed alone or inside a dict or list. The text after an element (its tail) is not included. Other objects are still stored as their `str()`. Captures are only active on the platform's own diagnostic runs (`SCRAPELY_DEBUG_CAPTURE=1`).
8
+
9
+ ## [0.1.33] - 2026-10-22
10
+
11
+ - Debug captures (`Actor.debug_capture`, `Actor.debug_capture_sync`) keep payloads over 256 KiB whole. They are stored gzip-compressed and base64-encoded as `{key, payload, encoding: 'gzip-base64', length, truncated}` instead of being cut at 262,144 characters; up to 8 Mi characters are kept. A payload that does not compress enough keeps its first 256 KiB, now marked `truncated: True` with its full `length`. Dict and list payloads are dumped as compact JSON, and `bytes` payloads are stored as UTF-8 text (they used to be stored as their `repr`). Captures are only active on the platform's own diagnostic runs (`SCRAPELY_DEBUG_CAPTURE=1`).
12
+
5
13
  ## [0.1.32] - 2026-10-20
6
14
 
7
15
  - Added `scrapely.scrapy`, the Scrapy integration: `run_scrapy_actor`, `initialize_logging`, `apply_apify_settings`, a scheduler backed by the run's request queue, a pipeline that pushes items to the default dataset, a proxy middleware, an HTTP cache storage and an extension that stops the crawl gracefully when the run is aborted. Queued Scrapy requests and cached responses are stored as JSON, never pickled, and a stored request only rebuilds as a `scrapy.Request` subclass. A request leaves the queue as handled only once Scrapy has finished it, so a migrated or aborted run does not lose or repeat it. Scrapy is imported only when these are used.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.32
3
+ Version: 0.1.34
4
4
  Summary: Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform
5
5
  Author-email: Scrapely <support@scrape.ly>
6
6
  License-Expression: MIT AND Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scrapely-python"
7
- version = "0.1.32"
7
+ version = "0.1.34"
8
8
  description = "Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform"
9
9
  readme = "README.md"
10
10
  license = "MIT AND Apache-2.0"
@@ -227,7 +227,7 @@ if TYPE_CHECKING: # Static analysers and IDEs see the lazy names as plain impor
227
227
  set_api_storage,
228
228
  )
229
229
 
230
- __version__ = '0.1.32'
230
+ __version__ = '0.1.34'
231
231
  __all__ = [
232
232
  # Core
233
233
  'Actor',
@@ -285,6 +285,53 @@ class _ActorMeta(type):
285
285
  await cls._shutdown()
286
286
 
287
287
 
288
+
289
+ def _element_markup(obj: Any) -> Optional[str]:
290
+ """The markup of a parsed XML/HTML element, or None when ``obj`` is not one.
291
+
292
+ Debug captures store the source an item was read from. Feed and page parsers hand over elements
293
+ (xml.etree / lxml elements and trees, BeautifulSoup tags, selectolax nodes, parsel / Scrapy
294
+ selectors); their ``str()`` is only a repr such as ``<Element 'change' at 0x...>``, which shows
295
+ nothing of the source. Optional parsers are recognised by module, never imported here.
296
+ """
297
+ import xml.etree.ElementTree as ET
298
+
299
+ if isinstance(obj, ET.ElementTree):
300
+ root = obj.getroot()
301
+ return ET.tostring(root, encoding='unicode') if root is not None else ''
302
+ if isinstance(obj, ET.Element):
303
+ if obj.tail: # the text after the element belongs to its parent, as with lxml's with_tail=False
304
+ import copy
305
+
306
+ obj = copy.copy(obj)
307
+ obj.tail = None
308
+ return ET.tostring(obj, encoding='unicode')
309
+ modules = [getattr(c, '__module__', '') or '' for c in type(obj).__mro__]
310
+ if any(m == 'lxml' or m.startswith('lxml.') for m in modules):
311
+ from lxml import etree # type: ignore[import-not-found]
312
+
313
+ out = etree.tostring(obj, encoding='unicode', with_tail=False)
314
+ return out if isinstance(out, str) else None
315
+ if any(m == 'bs4' or m.startswith('bs4.') for m in modules):
316
+ return str(obj)
317
+ if any(m == 'selectolax' or m.startswith('selectolax.') for m in modules):
318
+ html = getattr(obj, 'html', None)
319
+ return html if isinstance(html, str) else None
320
+ if any(m == 'parsel' or m.startswith('parsel.') for m in modules) and callable(getattr(obj, 'get', None)):
321
+ out = obj.get()
322
+ return out if isinstance(out, str) else None
323
+ return None
324
+
325
+
326
+ def _capture_json_default(obj: Any) -> Any:
327
+ """json.dumps fallback for debug captures: elements as their markup, anything else as str()."""
328
+ try:
329
+ markup = _element_markup(obj)
330
+ except Exception: # an unusual node type must not lose the rest of the payload
331
+ markup = None
332
+ return markup if markup is not None else str(obj)
333
+
334
+
288
335
  class Actor(metaclass=_ActorMeta):
289
336
  """Core class for building Actors on the Scrapely platform."""
290
337
 
@@ -925,7 +972,14 @@ class Actor(metaclass=_ActorMeta):
925
972
 
926
973
  # Debug capture: raw source payloads for platform diagnostics. Only active when the platform
927
974
  # sets SCRAPELY_DEBUG_CAPTURE=1 on its own diagnostic runs; a no-op in every other run.
975
+ # A payload up to _DEBUG_CAPTURE_MAX_BYTES characters is stored as it is; a larger one gzip-compressed
976
+ # (base64, at most _DEBUG_CAPTURE_MAX_BYTES characters again), keeping up to _DEBUG_CAPTURE_MAX_CHARS
977
+ # characters / _DEBUG_CAPTURE_MAX_ENCODED bytes of it. A plain cut at 256 KiB lost the part a check
978
+ # needed when a large API document starts with site navigation (the seller section of a 581 KB
979
+ # listing JSON sat at ~333 KB).
928
980
  _DEBUG_CAPTURE_MAX_BYTES = 256 * 1024
981
+ _DEBUG_CAPTURE_MAX_CHARS = 8 * 1024 * 1024
982
+ _DEBUG_CAPTURE_MAX_ENCODED = 8 * 1024 * 1024
929
983
  _DEBUG_CAPTURE_MAX_RECORDS = 500
930
984
  _debug_capture_count = 0
931
985
 
@@ -967,15 +1021,36 @@ class Actor(metaclass=_ActorMeta):
967
1021
  if cls._debug_capture_count >= cls._DEBUG_CAPTURE_MAX_RECORDS:
968
1022
  return
969
1023
  try:
970
- text = payload if isinstance(payload, str) else json.dumps(payload, default=str, ensure_ascii=False)
971
- if isinstance(text, bytes):
972
- text = text.decode('utf-8', 'replace')
973
- text = text[: cls._DEBUG_CAPTURE_MAX_BYTES]
1024
+ markup = _element_markup(payload)
1025
+ if markup is not None:
1026
+ text = markup
1027
+ elif isinstance(payload, (bytes, bytearray)):
1028
+ text = bytes(payload).decode('utf-8', 'replace')
1029
+ elif isinstance(payload, str):
1030
+ text = payload
1031
+ else:
1032
+ text = json.dumps(payload, default=_capture_json_default, ensure_ascii=False, separators=(',', ':'))
1033
+ record = cls._debug_capture_record(str(key), text)
974
1034
  cls._debug_capture_count += 1
975
- await cls.set_value(cls.debug_capture_record_key(key), {'key': str(key), 'payload': text})
1035
+ await cls.set_value(cls.debug_capture_record_key(key), record)
976
1036
  except Exception as err: # never break the run
977
1037
  logging.getLogger('scrapely').debug('debug_capture failed: %s', err)
978
1038
 
1039
+ @classmethod
1040
+ def _debug_capture_record(cls, key: str, text: str) -> Dict[str, Any]:
1041
+ """The record for one capture: plain when small, else gzip-base64 (plain head as the last resort)."""
1042
+ limit = cls._DEBUG_CAPTURE_MAX_BYTES
1043
+ if len(text) <= limit:
1044
+ return {'key': key, 'payload': text}
1045
+ import gzip
1046
+ raw = text[: cls._DEBUG_CAPTURE_MAX_CHARS].encode('utf-8')
1047
+ kept = raw[: cls._DEBUG_CAPTURE_MAX_ENCODED]
1048
+ packed = base64.b64encode(gzip.compress(kept, 6)).decode('ascii')
1049
+ if len(packed) <= limit:
1050
+ truncated = len(text) > cls._DEBUG_CAPTURE_MAX_CHARS or len(kept) < len(raw)
1051
+ return {'key': key, 'payload': packed, 'encoding': 'gzip-base64', 'length': len(text), 'truncated': truncated}
1052
+ return {'key': key, 'payload': text[:limit], 'length': len(text), 'truncated': True}
1053
+
979
1054
  @classmethod
980
1055
  def _resolve_open_args(
981
1056
  cls,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.32
3
+ Version: 0.1.34
4
4
  Summary: Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform
5
5
  Author-email: Scrapely <support@scrape.ly>
6
6
  License-Expression: MIT AND Apache-2.0