scrapely-python 0.1.19__tar.gz → 0.1.20__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.19/src/scrapely_python.egg-info → scrapely_python-0.1.20}/PKG-INFO +1 -1
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/pyproject.toml +1 -1
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/charging.py +21 -3
- {scrapely_python-0.1.19 → scrapely_python-0.1.20/src/scrapely_python.egg-info}/PKG-INFO +1 -1
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_charge_limit_guard.py +62 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/LICENSE +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/NOTICE +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/README.md +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/setup.cfg +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/actor.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/solver.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely_python.egg-info/top_level.txt +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_api_client_retries.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_apify_parity.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_browser_configuration.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_cdp_retry.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_charge_retries.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_charging_parity.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_charging_pending.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_input_proxy_aliases.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_parity.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_patch_browser_pool.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_proxy_configuration.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_push_idempotency.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_run_control.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_secrets.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_solver.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/tests/test_storage.py +0 -0
- {scrapely_python-0.1.19 → scrapely_python-0.1.20}/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.20"
|
|
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"
|
|
@@ -246,6 +246,8 @@ class ChargingManager:
|
|
|
246
246
|
# grows), so the local budget gates on the run's true remaining budget.
|
|
247
247
|
self._platform_other_usd: Decimal = Decimal('0')
|
|
248
248
|
self._last_remaining_budget_nd: Optional[int] = None
|
|
249
|
+
# True once the platform reported a remaining budget (its usage is then in the local budget).
|
|
250
|
+
self._platform_budget_seen = False
|
|
249
251
|
self._charge_lock = RLock()
|
|
250
252
|
self._flush_lock = asyncio.Lock()
|
|
251
253
|
# Serializes budget calculation/reservation with PPE-aware dataset pushes.
|
|
@@ -311,6 +313,7 @@ class ChargingManager:
|
|
|
311
313
|
self._last_remaining_budget_nd = None
|
|
312
314
|
if nd is None or self._max_total_charge_usd is None or nd < 0:
|
|
313
315
|
return
|
|
316
|
+
self._platform_budget_seen = True
|
|
314
317
|
other = self._max_total_charge_usd - Decimal(nd) / Decimal(1_000_000_000) - self._total_charged_usd
|
|
315
318
|
if other > self._platform_other_usd:
|
|
316
319
|
self._platform_other_usd = other
|
|
@@ -1717,7 +1720,7 @@ async def push_data_with_charges(
|
|
|
1717
1720
|
else:
|
|
1718
1721
|
await charging_manager._void_charge(name, op_id, charged)
|
|
1719
1722
|
|
|
1720
|
-
for _attempt in range(
|
|
1723
|
+
for _attempt in range(6):
|
|
1721
1724
|
if rows <= 0:
|
|
1722
1725
|
break
|
|
1723
1726
|
ops = []
|
|
@@ -1741,10 +1744,25 @@ async def push_data_with_charges(
|
|
|
1741
1744
|
if short_to is None:
|
|
1742
1745
|
break
|
|
1743
1746
|
# Some event came back short: void this attempt in full and retry with
|
|
1744
|
-
# the rows
|
|
1747
|
+
# the whole rows the budget covers at the FULL per-row cost. Sizing
|
|
1748
|
+
# from the short event alone ignores the other events of the row, so
|
|
1749
|
+
# the retry came back short again (appstore 64df0a3608d74af88: 20
|
|
1750
|
+
# details -> 14, retry sized 14, app-item short, 0 saved). What this
|
|
1751
|
+
# attempt was accepted is budget the void gives back; with the
|
|
1752
|
+
# platform's remainingBudgetNd the local budget also knows the rest.
|
|
1753
|
+
attempt_spent = sum(
|
|
1754
|
+
(charging_manager._calculate_event_price(name) or Decimal('0')) * Decimal(charged)
|
|
1755
|
+
for name, charged, _op, _unc in ops
|
|
1756
|
+
)
|
|
1745
1757
|
await undo(ops)
|
|
1746
1758
|
ops = []
|
|
1747
|
-
|
|
1759
|
+
fit = int(attempt_spent / row_cost) if row_cost > 0 else 0
|
|
1760
|
+
if charging_manager._platform_budget_seen and charging_manager._max_total_charge_usd is not None and row_cost > 0:
|
|
1761
|
+
fit = max(fit, int(charging_manager._remaining_budget_usd() / row_cost))
|
|
1762
|
+
# At least one more single-row try: without the platform's remaining
|
|
1763
|
+
# budget the leftover after the short answer is unknown, and the last
|
|
1764
|
+
# affordable row may still fit.
|
|
1765
|
+
rows = min(rows - 1, max(fit, 1))
|
|
1748
1766
|
|
|
1749
1767
|
if rows <= 0 or not ops:
|
|
1750
1768
|
await undo(ops)
|
|
@@ -585,3 +585,65 @@ async def test_platform_other_spend_never_shrinks() -> None:
|
|
|
585
585
|
api.usage = Decimal('0.10') # a stale or lower report
|
|
586
586
|
await mgr.charge('result', 1)
|
|
587
587
|
assert mgr._platform_other_usd == seen
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ---- multi-event retry sizing: whole rows at the FULL per-row cost ----
|
|
592
|
+
|
|
593
|
+
APP_PRICES = {'app-details': Decimal('0.00015'), 'app-item': Decimal('0.00005')} # row = $0.0002
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class RemainingMultiApi(MultiApi):
|
|
597
|
+
"""MultiApi that also reports remainingBudgetNd (server #725)."""
|
|
598
|
+
|
|
599
|
+
async def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, **kw: Any) -> Dict[str, Any]:
|
|
600
|
+
body = await super().post(path, json_data=json_data, headers=headers, **kw)
|
|
601
|
+
if not path.endswith('/charge/void') and self.max_usd is not None:
|
|
602
|
+
body = {**body, 'remainingBudgetNd': int((self.max_usd - self.spent) * 1_000_000_000)}
|
|
603
|
+
return body
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _app(server_max: str, remaining: bool = False):
|
|
607
|
+
api = (RemainingMultiApi if remaining else MultiApi)(APP_PRICES, Decimal(server_max))
|
|
608
|
+
return api, _manager(api, prices=dict(APP_PRICES), max_usd='1')
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@pytest.mark.asyncio
|
|
612
|
+
async def test_retry_after_one_short_event_is_sized_by_the_full_row_cost() -> None:
|
|
613
|
+
"""appstore 64df0a3608d74af88: details 20 -> 14 accepted; the retry must not size from 14."""
|
|
614
|
+
api, mgr = _app('0.0021')
|
|
615
|
+
store = Store()
|
|
616
|
+
r = await push_data_with_charges(mgr, [{'n': n} for n in range(20)], {'app-details': 1, 'app-item': 1}, True, store.push)
|
|
617
|
+
assert r.charged_count == 10 # $0.0021 / $0.0002 per full row
|
|
618
|
+
assert len(store.items) == 10
|
|
619
|
+
assert api.spent == Decimal('0.0020')
|
|
620
|
+
assert sorted(api.ops.values()) == [('app-details', 10), ('app-item', 10)]
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@pytest.mark.asyncio
|
|
624
|
+
async def test_retry_when_several_events_come_back_short() -> None:
|
|
625
|
+
"""Both events of the row run short on the way down; it still converges on whole paid rows."""
|
|
626
|
+
api, mgr = _multi('1', server_max='0.0405') # 2.53 rows of $0.016 (1 result + 3 pages)
|
|
627
|
+
store = Store()
|
|
628
|
+
r = await push_data_with_charges(mgr, [{'n': n} for n in range(6)], {'result': 1, 'page': 3}, True, store.push)
|
|
629
|
+
assert r.charged_count == 2
|
|
630
|
+
assert len(store.items) == 2
|
|
631
|
+
assert api.spent == Decimal('0.032')
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@pytest.mark.asyncio
|
|
635
|
+
async def test_last_affordable_row_is_saved_without_remaining_budget() -> None:
|
|
636
|
+
api, mgr = _app('0.0002') # exactly one row; the short answer hides the leftover
|
|
637
|
+
store = Store()
|
|
638
|
+
r = await push_data_with_charges(mgr, [{'n': n} for n in range(5)], {'app-details': 1, 'app-item': 1}, True, store.push)
|
|
639
|
+
assert r.charged_count == 1 and len(store.items) == 1
|
|
640
|
+
assert api.spent == Decimal('0.0002')
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@pytest.mark.asyncio
|
|
644
|
+
async def test_platform_remaining_budget_sizes_the_retry_exactly() -> None:
|
|
645
|
+
api, mgr = _app('0.00215', remaining=True) # 10.75 rows
|
|
646
|
+
store = Store()
|
|
647
|
+
r = await push_data_with_charges(mgr, [{'n': n} for n in range(20)], {'app-details': 1, 'app-item': 1}, True, store.push)
|
|
648
|
+
assert r.charged_count == 10 and len(store.items) == 10
|
|
649
|
+
assert api.spent == Decimal('0.0020')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/_file_system/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_kvs_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_rq_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely/storage/scrapely_storage_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.19 → scrapely_python-0.1.20}/src/scrapely_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|