scrapely-python 0.1.15__tar.gz → 0.1.17__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.
- {scrapely_python-0.1.15/src/scrapely_python.egg-info → scrapely_python-0.1.17}/PKG-INFO +1 -1
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/pyproject.toml +1 -1
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/actor.py +28 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/charging.py +61 -11
- scrapely_python-0.1.17/src/scrapely/solver.py +875 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17/src/scrapely_python.egg-info}/PKG-INFO +1 -1
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely_python.egg-info/SOURCES.txt +2 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_charge_limit_guard.py +111 -0
- scrapely_python-0.1.17/tests/test_solver.py +327 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/LICENSE +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/NOTICE +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/README.md +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/setup.cfg +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/src/scrapely_python.egg-info/top_level.txt +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_api_client_retries.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_apify_parity.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_browser_configuration.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_cdp_retry.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_charge_retries.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_charging_parity.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_charging_pending.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_input_proxy_aliases.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_parity.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_patch_browser_pool.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_proxy_configuration.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_push_idempotency.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_run_control.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_secrets.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_storage.py +0 -0
- {scrapely_python-0.1.15 → scrapely_python-0.1.17}/tests/test_terminal_status_message.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scrapely-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.17"
|
|
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"
|
|
@@ -1080,6 +1080,34 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1080
1080
|
status_message=data.get('statusMessage'),
|
|
1081
1081
|
)
|
|
1082
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
|
+
|
|
1083
1111
|
@classmethod
|
|
1084
1112
|
async def _put_run_status_message(
|
|
1085
1113
|
cls, message: str, *, is_terminal: bool
|
|
@@ -270,6 +270,13 @@ class ChargingManager:
|
|
|
270
270
|
self._charge_limit_hit = False
|
|
271
271
|
# Default-dataset rows refused after the limit (one warning, then a count).
|
|
272
272
|
self._refused_default_dataset_items = 0
|
|
273
|
+
# Prepaid-row balance: priced events charged through charge() minus rows
|
|
274
|
+
# pushed to the default dataset without a per-row charge. An Actor that
|
|
275
|
+
# charges first and then pushes plainly (charge('product'); push_data(row))
|
|
276
|
+
# has a positive balance after the charge, so the row it already paid for
|
|
277
|
+
# is still saved when that charge used up the spending limit. Push-then-
|
|
278
|
+
# charge Actors never go positive, so they get nothing extra for free.
|
|
279
|
+
self._prepaid_row_balance = 0
|
|
273
280
|
|
|
274
281
|
def close(self) -> None:
|
|
275
282
|
"""Mark the manager closed; subsequent charge/push operations are rejected."""
|
|
@@ -323,8 +330,40 @@ class ChargingManager:
|
|
|
323
330
|
)
|
|
324
331
|
self._refused_default_dataset_items += count
|
|
325
332
|
|
|
333
|
+
def _note_prepaid_charge(self, event_name: str, charged_count: int) -> None:
|
|
334
|
+
if (
|
|
335
|
+
charged_count > 0
|
|
336
|
+
and not is_default_dataset_item_event(event_name)
|
|
337
|
+
and self._per_event_prices.get(event_name, 0) > 0
|
|
338
|
+
):
|
|
339
|
+
self._prepaid_row_balance += charged_count
|
|
340
|
+
|
|
341
|
+
def _take_unpaid_rows(self, item_count: int) -> int:
|
|
342
|
+
"""Account for ``item_count`` rows pushed without a per-row charge.
|
|
343
|
+
|
|
344
|
+
Returns how many of them may be stored: all of them while the limit is
|
|
345
|
+
not reached, afterwards only the rows already paid for by earlier
|
|
346
|
+
charges (the positive prepaid balance).
|
|
347
|
+
"""
|
|
348
|
+
if item_count <= 0:
|
|
349
|
+
return 0
|
|
350
|
+
if not self.is_charge_limit_reached():
|
|
351
|
+
self._prepaid_row_balance -= item_count
|
|
352
|
+
return item_count
|
|
353
|
+
allowed = max(0, min(item_count, self._prepaid_row_balance))
|
|
354
|
+
self._prepaid_row_balance -= allowed
|
|
355
|
+
return allowed
|
|
356
|
+
|
|
326
357
|
def _refuse_default_dataset_push(self, item_count: int) -> bool:
|
|
327
|
-
if item_count <= 0 or
|
|
358
|
+
if item_count <= 0 or self._pricing_info is None:
|
|
359
|
+
return False
|
|
360
|
+
if not self.is_charge_limit_reached():
|
|
361
|
+
self._take_unpaid_rows(item_count)
|
|
362
|
+
return False
|
|
363
|
+
# All-or-nothing here (storage clients cannot store a slice): store the
|
|
364
|
+
# batch only when earlier charges paid for every row of it.
|
|
365
|
+
if self._prepaid_row_balance >= item_count:
|
|
366
|
+
self._prepaid_row_balance -= item_count
|
|
328
367
|
return False
|
|
329
368
|
self._note_refused_default_dataset_items(item_count)
|
|
330
369
|
return True
|
|
@@ -851,7 +890,9 @@ class ChargingManager:
|
|
|
851
890
|
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
|
|
852
891
|
"""Atomically reserve and submit one logical charge operation."""
|
|
853
892
|
async with self._operation_lock:
|
|
854
|
-
|
|
893
|
+
result = await self._charge_exclusive(event_name, count)
|
|
894
|
+
self._note_prepaid_charge(event_name, result.charged_count)
|
|
895
|
+
return result
|
|
855
896
|
|
|
856
897
|
async def _charge_exclusive(self, event_name: str, count: int = 1) -> ChargeResult:
|
|
857
898
|
if self._closed:
|
|
@@ -1327,19 +1368,27 @@ async def push_data_and_charge(
|
|
|
1327
1368
|
# default-dataset push that does not pay per row (no event, or only
|
|
1328
1369
|
# free events) stores nothing. Paid pushes go on below, where the
|
|
1329
1370
|
# budget cap stores only the rows that are actually charged.
|
|
1371
|
+
# Rows the Actor already paid for with an earlier charge() (charge first,
|
|
1372
|
+
# then a plain push) are still stored: see ChargingManager._take_unpaid_rows.
|
|
1373
|
+
# The result still reports the limit, so the Actor stops after them.
|
|
1374
|
+
unpaid_past_limit = False
|
|
1330
1375
|
if (
|
|
1331
1376
|
is_ppe
|
|
1332
1377
|
and is_default_dataset
|
|
1333
1378
|
and items
|
|
1334
|
-
and charging_manager.is_charge_limit_reached()
|
|
1335
1379
|
and not _push_is_paid_per_item(charging_manager, event_name, resolved_event)
|
|
1336
1380
|
):
|
|
1337
|
-
charging_manager.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1381
|
+
unpaid_past_limit = charging_manager.is_charge_limit_reached()
|
|
1382
|
+
allowed = charging_manager._take_unpaid_rows(len(items))
|
|
1383
|
+
if allowed < len(items):
|
|
1384
|
+
charging_manager._note_refused_default_dataset_items(len(items) - allowed)
|
|
1385
|
+
if allowed == 0:
|
|
1386
|
+
return ChargeResult(
|
|
1387
|
+
chargeable_within_limit=charging_manager.compute_chargeable(),
|
|
1388
|
+
charged_count=0,
|
|
1389
|
+
event_charge_limit_reached=True,
|
|
1390
|
+
)
|
|
1391
|
+
items = items[:allowed]
|
|
1343
1392
|
|
|
1344
1393
|
limited_count = len(items)
|
|
1345
1394
|
if is_ppe and resolved_event:
|
|
@@ -1522,7 +1571,8 @@ async def push_data_and_charge(
|
|
|
1522
1571
|
event_charge_limit_reached=any(
|
|
1523
1572
|
result.event_charge_limit_reached for result in results
|
|
1524
1573
|
)
|
|
1525
|
-
or accepted_for_items < len(limited_items)
|
|
1574
|
+
or accepted_for_items < len(limited_items)
|
|
1575
|
+
or unpaid_past_limit,
|
|
1526
1576
|
)
|
|
1527
1577
|
|
|
1528
1578
|
|
|
@@ -1531,7 +1581,7 @@ async def push_data_and_charge(
|
|
|
1531
1581
|
return ChargeResult(
|
|
1532
1582
|
chargeable_within_limit=charging_manager.compute_chargeable(),
|
|
1533
1583
|
charged_count=0,
|
|
1534
|
-
event_charge_limit_reached=
|
|
1584
|
+
event_charge_limit_reached=unpaid_past_limit,
|
|
1535
1585
|
)
|
|
1536
1586
|
|
|
1537
1587
|
|