scrapely-python 0.1.14__tar.gz → 0.1.16__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.14/src/scrapely_python.egg-info → scrapely_python-0.1.16}/PKG-INFO +1 -1
  2. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/pyproject.toml +1 -1
  3. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/__init__.py +1 -1
  4. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/actor.py +41 -1
  5. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/charging.py +13 -3
  6. scrapely_python-0.1.16/src/scrapely/solver.py +875 -0
  7. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/api_client.py +125 -26
  8. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/client.py +5 -1
  9. {scrapely_python-0.1.14 → scrapely_python-0.1.16/src/scrapely_python.egg-info}/PKG-INFO +1 -1
  10. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/SOURCES.txt +3 -0
  11. scrapely_python-0.1.16/tests/test_push_idempotency.py +242 -0
  12. scrapely_python-0.1.16/tests/test_solver.py +327 -0
  13. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_terminal_status_message.py +19 -0
  14. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/LICENSE +0 -0
  15. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/LICENSE-APACHE +0 -0
  16. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/NOTICE +0 -0
  17. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/README.md +0 -0
  18. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/setup.cfg +0 -0
  19. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/_browser_locations.py +0 -0
  20. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/browser_configuration.py +0 -0
  21. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/cdp_retry.py +0 -0
  22. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/charging_client.py +0 -0
  23. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/configuration.py +0 -0
  24. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/crypto/__init__.py +0 -0
  25. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/crypto/_secrets.py +0 -0
  26. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/errors.py +0 -0
  27. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/events/__init__.py +0 -0
  28. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/events/_types.py +0 -0
  29. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/events/event_manager.py +0 -0
  30. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/log.py +0 -0
  31. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/patch_browser_pool.py +0 -0
  32. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/pricing_types.py +0 -0
  33. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/proxy_configuration.py +0 -0
  34. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/py.typed +0 -0
  35. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/request_loaders/__init__.py +0 -0
  36. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
  37. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/__init__.py +0 -0
  38. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_file_system/__init__.py +0 -0
  39. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
  40. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
  41. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_metadata.py +0 -0
  42. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/patch_storage.py +0 -0
  43. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
  44. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
  45. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_rq_client.py +0 -0
  46. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_storage_client.py +0 -0
  47. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
  48. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/types.py +0 -0
  49. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/utils.py +0 -0
  50. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage_clients/__init__.py +0 -0
  51. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storages/__init__.py +0 -0
  52. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/types.py +0 -0
  53. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/webhook.py +0 -0
  54. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
  55. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/requires.txt +0 -0
  56. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/top_level.txt +0 -0
  57. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_api_client_retries.py +0 -0
  58. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_apify_parity.py +0 -0
  59. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_browser_configuration.py +0 -0
  60. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_cdp_retry.py +0 -0
  61. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charge_limit_guard.py +0 -0
  62. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charge_retries.py +0 -0
  63. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charging_parity.py +0 -0
  64. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charging_pending.py +0 -0
  65. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_input_proxy_aliases.py +0 -0
  66. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_parity.py +0 -0
  67. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_patch_browser_pool.py +0 -0
  68. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_proxy_configuration.py +0 -0
  69. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_run_control.py +0 -0
  70. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_secrets.py +0 -0
  71. {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.14
3
+ Version: 0.1.16
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.14"
7
+ version = "0.1.16"
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"
@@ -132,7 +132,7 @@ from .errors import (
132
132
  raise_for_status,
133
133
  )
134
134
 
135
- __version__ = '0.1.14'
135
+ __version__ = '0.1.16'
136
136
  __all__ = [
137
137
  # Core
138
138
  'Actor',
@@ -1054,7 +1054,19 @@ class Actor(metaclass=_ActorMeta):
1054
1054
  else:
1055
1055
  logger.info(logged)
1056
1056
 
1057
- result = await cls._put_run_status_message(message, is_terminal=is_terminal)
1057
+ if is_terminal:
1058
+ result = await cls._put_run_status_message(message, is_terminal=True)
1059
+ else:
1060
+ # A progress update is best effort: if the API still fails after the
1061
+ # retry window (e.g. a long 503), log it and carry on instead of
1062
+ # crashing the run. Terminal updates keep raising.
1063
+ try:
1064
+ result = await cls._put_run_status_message(message, is_terminal=False)
1065
+ except asyncio.CancelledError:
1066
+ raise
1067
+ except Exception as e: # noqa: BLE001
1068
+ logger.warning('Failed to update the status message (continuing): %s', e)
1069
+ return ActorRun(id='', actor_id='', status='')
1058
1070
  if result is None:
1059
1071
  return ActorRun(id='', actor_id='', status='')
1060
1072
  run_id, data = result
@@ -1068,6 +1080,34 @@ class Actor(metaclass=_ActorMeta):
1068
1080
  status_message=data.get('statusMessage'),
1069
1081
  )
1070
1082
 
1083
+ @classmethod
1084
+ async def solver_status(cls, vendor: str) -> Dict[str, Any]:
1085
+ """Anti-bot solver status for a vendor (free, cached for 15 s, never raises).
1086
+
1087
+ ``usable`` False means the vendor cannot be solved right now: take your
1088
+ fallback (for example the hosted browser), or fail the items that need
1089
+ a solve at once and call ``set_solver_degraded_status()``. See
1090
+ ``scrapely.solver``.
1091
+ """
1092
+ from .solver import solver_status_async
1093
+
1094
+ return await solver_status_async(vendor)
1095
+
1096
+ @classmethod
1097
+ async def set_solver_degraded_status(cls, skipped: Optional[int] = None, total: Optional[int] = None) -> None:
1098
+ """Set the standard status message for a run whose site protection cannot be solved right now.
1099
+
1100
+ "Site protection temporarily unavailable, results may be incomplete",
1101
+ with "(N of M items skipped)" when both counts are given. Never raises:
1102
+ the run goes on.
1103
+ """
1104
+ from .solver import degraded_status_message
1105
+
1106
+ try:
1107
+ await cls.set_status_message(degraded_status_message(skipped, total))
1108
+ except Exception as exc: # noqa: BLE001 - a status message must never stop the run
1109
+ logger.warning(f'Could not set the solver status message: {exc}')
1110
+
1071
1111
  @classmethod
1072
1112
  async def _put_run_status_message(
1073
1113
  cls, message: str, *, is_terminal: bool
@@ -52,7 +52,7 @@ _PENDING_MAX_ENTRIES = 1024
52
52
  # Same-key POST attempts before leaving an intent pending / re-queueing a void.
53
53
  # The API client itself retries 429 and 5xx answers and refused or reset
54
54
  # connections of these POSTs with exponential backoff (honouring Retry-After)
55
- # for its retry window (2 minutes, SCRAPELY_API_RETRY_WINDOW_SECS): re-sending
55
+ # for its retry window (5 minutes, SCRAPELY_API_RETRY_WINDOW_SECS): re-sending
56
56
  # is safe because every attempt carries the same Idempotency-Key. These outer
57
57
  # attempts cover what the client gives up on quickly (timeouts, other
58
58
  # transport errors), within _RETRY_BUDGET_SECS. Then the charge stays pending
@@ -62,7 +62,7 @@ _POST_ATTEMPTS = 8
62
62
 
63
63
  # Outer retries stop once this much time has passed since the first attempt
64
64
  # (the API retry window; JS SDK parity).
65
- _RETRY_BUDGET_SECS = 120.0
65
+ _RETRY_BUDGET_SECS = 300.0
66
66
 
67
67
 
68
68
  def _is_transient_status(status: Optional[int]) -> bool:
@@ -1567,10 +1567,20 @@ def _push_certainly_failed(err: BaseException) -> bool:
1567
1567
  """True when a dataset push certainly stored nothing.
1568
1568
 
1569
1569
  The storage API answered with an error status, or the connection was never
1570
- made. Timeouts and resets after sending are not proof: the write may have landed.
1570
+ made. Timeouts and resets after sending are not proof: the write may have landed,
1571
+ and neither is any error of a push one of whose earlier attempts may have been
1572
+ applied (``outcome_unknown``, set by the storage client's retries).
1571
1573
  """
1572
1574
  import httpx
1573
1575
 
1576
+ c: Optional[BaseException] = err
1577
+ for _ in range(4):
1578
+ if c is None:
1579
+ break
1580
+ if getattr(c, 'outcome_unknown', False) is True:
1581
+ return False
1582
+ c = c.__cause__ or c.__context__
1583
+
1574
1584
  e: Optional[BaseException] = err
1575
1585
  for _ in range(4):
1576
1586
  if e is None: