scrapely-python 0.1.20__tar.gz → 0.1.22__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.20/src/scrapely_python.egg-info → scrapely_python-0.1.22}/PKG-INFO +1 -1
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/pyproject.toml +1 -1
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/actor.py +26 -4
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/charging.py +18 -7
- {scrapely_python-0.1.20 → scrapely_python-0.1.22/src/scrapely_python.egg-info}/PKG-INFO +1 -1
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_charge_limit_guard.py +176 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/LICENSE +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/NOTICE +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/README.md +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/setup.cfg +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/solver.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/top_level.txt +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_api_client_retries.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_apify_parity.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_browser_configuration.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_cdp_retry.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_charge_retries.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_charging_parity.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_charging_pending.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_input_proxy_aliases.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_parity.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_patch_browser_pool.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_proxy_configuration.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_push_idempotency.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_run_control.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_secrets.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_solver.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/tests/test_storage.py +0 -0
- {scrapely_python-0.1.20 → scrapely_python-0.1.22}/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.22"
|
|
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"
|
|
@@ -783,6 +783,8 @@ class Actor(metaclass=_ActorMeta):
|
|
|
783
783
|
*,
|
|
784
784
|
event_name: Optional[str] = None,
|
|
785
785
|
charges: Optional[Dict[str, int]] = None,
|
|
786
|
+
charge_per: str = 'row',
|
|
787
|
+
dataset: Optional[Union[str, Dataset]] = None,
|
|
786
788
|
) -> ChargeResult:
|
|
787
789
|
"""Push data to the default dataset, charging pay-per-event items.
|
|
788
790
|
|
|
@@ -802,6 +804,16 @@ class Actor(metaclass=_ActorMeta):
|
|
|
802
804
|
charges 1 ``result`` and 3 ``page`` events for every row. A row is stored
|
|
803
805
|
only when all of its events were charged; rows the spending limit cannot
|
|
804
806
|
pay for in full are not stored. Cannot be combined with ``charged_event_name``.
|
|
807
|
+
charge_per: ``'row'`` (default) charges ``charges`` for every row; ``'batch'``
|
|
808
|
+
charges them once for the whole push, e.g.
|
|
809
|
+
``push_data(page_rows, charges={'document': 1}, charge_per='batch')``:
|
|
810
|
+
all rows are stored if the charge is accepted, none if it is refused.
|
|
811
|
+
dataset: Push to this named dataset (name, or a Dataset from ``open_dataset``)
|
|
812
|
+
instead of the default one, with the same charge-first semantics:
|
|
813
|
+
``push_data(rows, charged_event_name='comment', dataset=f'comments-{run_id}')``.
|
|
814
|
+
Rows of a named dataset carry no implicit default-dataset-item event and are
|
|
815
|
+
not gated by the default dataset's spending-limit safety net; charged pushes
|
|
816
|
+
store only the rows that were charged.
|
|
805
817
|
|
|
806
818
|
Returns:
|
|
807
819
|
ChargeResult describing how many items were charged and pushed.
|
|
@@ -822,6 +834,13 @@ class Actor(metaclass=_ActorMeta):
|
|
|
822
834
|
if cls._default_dataset is None:
|
|
823
835
|
cls._default_dataset = await cls.open_dataset()
|
|
824
836
|
|
|
837
|
+
target = cls._default_dataset
|
|
838
|
+
if dataset is not None:
|
|
839
|
+
target = dataset if isinstance(dataset, Dataset) else await cls.open_dataset(name=dataset)
|
|
840
|
+
is_default = target is cls._default_dataset or (
|
|
841
|
+
getattr(target, 'id', None) is not None and target.id == getattr(cls._default_dataset, 'id', None)
|
|
842
|
+
)
|
|
843
|
+
|
|
825
844
|
items = data if isinstance(data, list) else [data]
|
|
826
845
|
charging_manager = cls.get_charging_manager()
|
|
827
846
|
if charges is not None:
|
|
@@ -831,15 +850,18 @@ class Actor(metaclass=_ActorMeta):
|
|
|
831
850
|
charging_manager,
|
|
832
851
|
items,
|
|
833
852
|
charges,
|
|
834
|
-
|
|
835
|
-
|
|
853
|
+
is_default,
|
|
854
|
+
target.push_data,
|
|
855
|
+
charge_per=charge_per,
|
|
836
856
|
)
|
|
857
|
+
if charge_per != 'row':
|
|
858
|
+
raise ValueError("charge_per needs charges={...}")
|
|
837
859
|
return await push_data_and_charge(
|
|
838
860
|
charging_manager,
|
|
839
861
|
items,
|
|
840
862
|
event_name,
|
|
841
|
-
|
|
842
|
-
|
|
863
|
+
is_default,
|
|
864
|
+
target.push_data,
|
|
843
865
|
)
|
|
844
866
|
|
|
845
867
|
@classmethod
|
|
@@ -1657,9 +1657,15 @@ async def push_data_with_charges(
|
|
|
1657
1657
|
charges: Dict[str, int],
|
|
1658
1658
|
is_default_dataset: bool,
|
|
1659
1659
|
push_fn: Any,
|
|
1660
|
+
charge_per: str = 'row',
|
|
1660
1661
|
) -> ChargeResult:
|
|
1661
1662
|
"""Push rows that each cost several events, atomically per row.
|
|
1662
1663
|
|
|
1664
|
+
``charge_per='batch'``: ``charges`` are charged ONCE for the whole push (e.g. one
|
|
1665
|
+
``document`` for all of its page rows). If every event is accepted all rows are
|
|
1666
|
+
stored, otherwise none (and nothing stays billed). The prepaid-row balance is
|
|
1667
|
+
neither used nor changed.
|
|
1668
|
+
|
|
1663
1669
|
``charges`` maps event name -> events per row, e.g. ``{'result': 1, 'page': 3}``.
|
|
1664
1670
|
A row is stored only when every one of its events was charged: the full set
|
|
1665
1671
|
is charged first, rows the budget cannot fully pay for are not stored, and
|
|
@@ -1669,6 +1675,9 @@ async def push_data_with_charges(
|
|
|
1669
1675
|
dataset the default-dataset-item event (when priced) is added per row.
|
|
1670
1676
|
"""
|
|
1671
1677
|
charges = _validate_row_charges(charges)
|
|
1678
|
+
if charge_per not in ('row', 'batch'):
|
|
1679
|
+
raise ValueError("charge_per must be 'row' or 'batch'")
|
|
1680
|
+
batch = charge_per == 'batch'
|
|
1672
1681
|
rows_in = items if isinstance(items, list) else [items]
|
|
1673
1682
|
if not rows_in:
|
|
1674
1683
|
return ChargeResult(chargeable_within_limit={}, charged_count=0, event_charge_limit_reached=False)
|
|
@@ -1691,7 +1700,8 @@ async def push_data_with_charges(
|
|
|
1691
1700
|
resolve_default_dataset_item_event_name(charging_manager._per_event_prices)
|
|
1692
1701
|
if default_event and default_event not in per_row and \
|
|
1693
1702
|
(charging_manager._calculate_event_price(default_event) or Decimal('0')) > 0:
|
|
1694
|
-
|
|
1703
|
+
# Per stored row, also for a batch.
|
|
1704
|
+
per_row[default_event] = len(rows_in) if batch else 1
|
|
1695
1705
|
if not per_row:
|
|
1696
1706
|
# Every listed event is free: a plain (unpaid) push.
|
|
1697
1707
|
return await _unpaid_push_locked(charging_manager, rows_in, is_default_dataset, push_fn)
|
|
@@ -1702,7 +1712,8 @@ async def push_data_with_charges(
|
|
|
1702
1712
|
row_cost = sum(
|
|
1703
1713
|
(charging_manager._calculate_event_price(n) or Decimal('0')) * Decimal(c) for n, c in per_row.items()
|
|
1704
1714
|
)
|
|
1705
|
-
rows
|
|
1715
|
+
# Units charged for: rows, or the one batch (row_cost is then the batch's cost).
|
|
1716
|
+
rows = 1 if batch else len(rows_in)
|
|
1706
1717
|
if charging_manager._max_total_charge_usd is not None and row_cost > 0:
|
|
1707
1718
|
remaining = charging_manager._remaining_budget_usd()
|
|
1708
1719
|
rows = min(rows, max(0, int(remaining / row_cost)))
|
|
@@ -1775,11 +1786,11 @@ async def push_data_with_charges(
|
|
|
1775
1786
|
event_charge_limit_reached=True,
|
|
1776
1787
|
)
|
|
1777
1788
|
|
|
1778
|
-
kept = rows_in[:rows]
|
|
1779
|
-
if
|
|
1789
|
+
kept = rows_in if batch else rows_in[:rows]
|
|
1790
|
+
if len(kept) < len(rows_in):
|
|
1780
1791
|
charging_manager._mark_charge_limit_hit()
|
|
1781
1792
|
if is_default_dataset:
|
|
1782
|
-
charging_manager._note_refused_default_dataset_items(len(rows_in) -
|
|
1793
|
+
charging_manager._note_refused_default_dataset_items(len(rows_in) - len(kept))
|
|
1783
1794
|
try:
|
|
1784
1795
|
await _gated_push(push_fn, kept)
|
|
1785
1796
|
except Exception as push_err:
|
|
@@ -1789,8 +1800,8 @@ async def push_data_with_charges(
|
|
|
1789
1800
|
raise
|
|
1790
1801
|
return ChargeResult(
|
|
1791
1802
|
chargeable_within_limit=charging_manager.compute_chargeable(),
|
|
1792
|
-
charged_count=
|
|
1793
|
-
event_charge_limit_reached=
|
|
1803
|
+
charged_count=len(kept),
|
|
1804
|
+
event_charge_limit_reached=len(kept) < len(rows_in) or charging_manager.is_charge_limit_reached(),
|
|
1794
1805
|
)
|
|
1795
1806
|
|
|
1796
1807
|
|
|
@@ -647,3 +647,179 @@ async def test_platform_remaining_budget_sizes_the_retry_exactly() -> None:
|
|
|
647
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
648
|
assert r.charged_count == 10 and len(store.items) == 10
|
|
649
649
|
assert api.spent == Decimal('0.0020')
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# ---- batch-charged push: charges={'document': 1}, charge_per='batch' ----
|
|
653
|
+
|
|
654
|
+
DOC = {'document': Decimal('0.01')}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _doc(max_usd: str, server_max: Optional[str] = None, remaining: bool = False):
|
|
658
|
+
api = (RemainingMultiApi if remaining else MultiApi)(DOC, Decimal(server_max or max_usd))
|
|
659
|
+
return api, _manager(api, prices=dict(DOC), max_usd=max_usd)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _pages(n: int):
|
|
663
|
+
return [{'page': i} for i in range(n)]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
@pytest.mark.asyncio
|
|
667
|
+
async def test_batch_accepted_stores_every_row_for_one_charge() -> None:
|
|
668
|
+
api, mgr = _doc('1')
|
|
669
|
+
store = Store()
|
|
670
|
+
r = await push_data_with_charges(mgr, _pages(7), {'document': 1}, True, store.push, charge_per='batch')
|
|
671
|
+
assert r.charged_count == 7 and r.event_charge_limit_reached is False
|
|
672
|
+
assert len(store.items) == 7
|
|
673
|
+
assert api.spent == Decimal('0.01')
|
|
674
|
+
assert list(api.ops.values()) == [('document', 1)]
|
|
675
|
+
assert mgr._prepaid_row_balance == 0, 'batch push neither uses nor adds prepaid rows'
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@pytest.mark.asyncio
|
|
679
|
+
async def test_batch_refused_stores_nothing() -> None:
|
|
680
|
+
api, mgr = _doc('0.005')
|
|
681
|
+
store = Store()
|
|
682
|
+
r = await push_data_with_charges(mgr, _pages(4), {'document': 1}, True, store.push, charge_per='batch')
|
|
683
|
+
assert r.charged_count == 0 and r.event_charge_limit_reached is True
|
|
684
|
+
assert store.items == [] and api.spent == 0
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@pytest.mark.asyncio
|
|
688
|
+
async def test_batch_at_the_exact_budget_then_the_next_is_refused() -> None:
|
|
689
|
+
api, mgr = _doc('0.02')
|
|
690
|
+
store = Store()
|
|
691
|
+
for _ in range(2):
|
|
692
|
+
r = await push_data_with_charges(mgr, _pages(3), {'document': 1}, True, store.push, charge_per='batch')
|
|
693
|
+
assert r.charged_count == 3
|
|
694
|
+
assert mgr.is_charge_limit_reached() is True
|
|
695
|
+
r = await push_data_with_charges(mgr, _pages(3), {'document': 1}, True, store.push, charge_per='batch')
|
|
696
|
+
assert r.charged_count == 0
|
|
697
|
+
assert len(store.items) == 6 and api.spent == Decimal('0.02')
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@pytest.mark.asyncio
|
|
701
|
+
async def test_batch_respects_the_usage_aware_budget() -> None:
|
|
702
|
+
# Local cap $1, but the platform (usage) only has $0.015 left: first batch fits, the second is refused
|
|
703
|
+
# and its partial charge does not leave rows stored.
|
|
704
|
+
api, mgr = _doc('1', server_max='0.015', remaining=True)
|
|
705
|
+
store = Store()
|
|
706
|
+
r1 = await push_data_with_charges(mgr, _pages(2), {'document': 1}, True, store.push, charge_per='batch')
|
|
707
|
+
assert r1.charged_count == 2
|
|
708
|
+
assert mgr._platform_budget_seen is True
|
|
709
|
+
r2 = await push_data_with_charges(mgr, _pages(2), {'document': 1}, True, store.push, charge_per='batch')
|
|
710
|
+
assert r2.charged_count == 0
|
|
711
|
+
assert len(store.items) == 2 and api.spent == Decimal('0.01')
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
@pytest.mark.asyncio
|
|
715
|
+
async def test_batch_with_the_default_dataset_item_event_charges_it_per_row() -> None:
|
|
716
|
+
prices = {'document': Decimal('0.01'), 'apify-default-dataset-item': Decimal('0.001')}
|
|
717
|
+
api = MultiApi(prices, Decimal('1'))
|
|
718
|
+
mgr = _manager(api, prices=dict(prices), max_usd='1')
|
|
719
|
+
store = Store()
|
|
720
|
+
r = await push_data_with_charges(mgr, _pages(5), {'document': 1}, True, store.push, charge_per='batch')
|
|
721
|
+
assert r.charged_count == 5
|
|
722
|
+
assert api.spent == Decimal('0.01') # the platform derives the dataset-item event itself
|
|
723
|
+
assert mgr.get_charged_events().get('apify-default-dataset-item') == 5, 'counted per stored row locally'
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def test_charge_per_validation() -> None:
|
|
727
|
+
import asyncio
|
|
728
|
+
api, mgr = _doc('1')
|
|
729
|
+
with pytest.raises(ValueError):
|
|
730
|
+
asyncio.run(push_data_with_charges(mgr, _pages(1), {'document': 1}, True, Store().push, charge_per='page'))
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# ---- charged pushes to a NAMED dataset (add-on rows, e.g. comments-<runId>-<videoId>) ----
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@pytest.mark.asyncio
|
|
737
|
+
async def test_named_dataset_charged_push_stores_only_charged_rows() -> None:
|
|
738
|
+
api = FakeChargeApi(price=Decimal('0.01'), max_usd=Decimal('0.03'))
|
|
739
|
+
mgr = _manager(api)
|
|
740
|
+
named = Store()
|
|
741
|
+
r = await push_data_and_charge(mgr, [{'c': n} for n in range(5)], 'result', False, named.push)
|
|
742
|
+
assert r.charged_count == 3 and r.event_charge_limit_reached is True
|
|
743
|
+
assert [i['c'] for i in named.items] == [0, 1, 2]
|
|
744
|
+
assert api.spent == Decimal('0.03')
|
|
745
|
+
assert mgr._prepaid_row_balance == 0, 'paid named rows neither use nor add prepaid rows'
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@pytest.mark.asyncio
|
|
749
|
+
async def test_named_dataset_failed_push_voids_the_charge() -> None:
|
|
750
|
+
api = MultiApi({'comment': Decimal('0.01')}, Decimal('1'))
|
|
751
|
+
mgr = _manager(api, prices={'comment': Decimal('0.01')}, max_usd='1')
|
|
752
|
+
|
|
753
|
+
class Boom(HttpError):
|
|
754
|
+
pass
|
|
755
|
+
|
|
756
|
+
async def failing_push(rows: List[Dict[str, Any]]) -> None:
|
|
757
|
+
raise Boom(400, 'invalid item') # the storage API answered: nothing stored
|
|
758
|
+
|
|
759
|
+
with pytest.raises(Boom):
|
|
760
|
+
await push_data_with_charges(mgr, [{'c': 1}, {'c': 2}], {'comment': 1}, False, failing_push)
|
|
761
|
+
assert api.spent == 0 and api.voids, 'paid rows that were not stored are not billed'
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@pytest.mark.asyncio
|
|
765
|
+
async def test_named_dataset_multi_event_and_batch_modes() -> None:
|
|
766
|
+
api, mgr = _multi('0.04')
|
|
767
|
+
named = Store()
|
|
768
|
+
r = await push_data_with_charges(mgr, [{'n': n} for n in range(5)], {'result': 1, 'page': 3}, False, named.push)
|
|
769
|
+
assert r.charged_count == 2 and len(named.items) == 2
|
|
770
|
+
api2, mgr2 = _doc('1')
|
|
771
|
+
named2 = Store()
|
|
772
|
+
r2 = await push_data_with_charges(mgr2, _pages(4), {'document': 1}, False, named2.push, charge_per='batch')
|
|
773
|
+
assert r2.charged_count == 4 and len(named2.items) == 4 and api2.spent == Decimal('0.01')
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@pytest.mark.asyncio
|
|
777
|
+
async def test_named_dataset_plain_push_is_not_gated_by_the_default_safety_net() -> None:
|
|
778
|
+
api = FakeChargeApi(price=Decimal('0.01'), max_usd=Decimal('0.01'))
|
|
779
|
+
mgr = _manager(api, max_usd='0.01')
|
|
780
|
+
await mgr.charge('result', 1)
|
|
781
|
+
assert mgr.is_charge_limit_reached() is True
|
|
782
|
+
named = Store()
|
|
783
|
+
await push_data_and_charge(mgr, [{'meta': 1}], None, False, named.push)
|
|
784
|
+
assert len(named.items) == 1
|
|
785
|
+
assert mgr._prepaid_row_balance == 1, 'a named plain push does not use the default dataset prepaid rows'
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@pytest.mark.asyncio
|
|
789
|
+
async def test_actor_push_data_routes_dataset_argument(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
790
|
+
from scrapely import actor as actor_mod
|
|
791
|
+
calls: List[Any] = []
|
|
792
|
+
|
|
793
|
+
class FakeDs:
|
|
794
|
+
def __init__(self, ds_id: str) -> None:
|
|
795
|
+
self.id = ds_id
|
|
796
|
+
self.rows: List[Any] = []
|
|
797
|
+
|
|
798
|
+
async def push_data(self, rows: Any) -> None:
|
|
799
|
+
self.rows.extend(rows)
|
|
800
|
+
|
|
801
|
+
default, named = FakeDs('default-id'), FakeDs('named-id')
|
|
802
|
+
A = actor_mod.Actor
|
|
803
|
+
monkeypatch.setattr(A, '_ensure_initialized', classmethod(lambda cls, name: None))
|
|
804
|
+
monkeypatch.setattr(A, '_default_dataset', default)
|
|
805
|
+
|
|
806
|
+
async def fake_open(cls, name_or_id=None, options=None, *, id=None, name=None, alias=None, force_cloud=False):
|
|
807
|
+
calls.append(name)
|
|
808
|
+
return named
|
|
809
|
+
|
|
810
|
+
monkeypatch.setattr(A, 'open_dataset', classmethod(fake_open))
|
|
811
|
+
monkeypatch.setattr(actor_mod, 'Dataset', FakeDs)
|
|
812
|
+
seen: List[Any] = []
|
|
813
|
+
|
|
814
|
+
async def fake_pdc(mgr, items, event, is_default, push_fn):
|
|
815
|
+
seen.append((event, is_default))
|
|
816
|
+
await push_fn(items)
|
|
817
|
+
return 'ok'
|
|
818
|
+
|
|
819
|
+
monkeypatch.setattr(actor_mod, 'push_data_and_charge', fake_pdc)
|
|
820
|
+
monkeypatch.setattr(A, 'get_charging_manager', classmethod(lambda cls: None))
|
|
821
|
+
await A.push_data([{'a': 1}], charged_event_name='comment', dataset='comments-run-1')
|
|
822
|
+
await A.push_data([{'b': 1}], charged_event_name='result')
|
|
823
|
+
assert calls == ['comments-run-1']
|
|
824
|
+
assert seen == [('comment', False), ('result', True)]
|
|
825
|
+
assert named.rows == [{'a': 1}] and default.rows == [{'b': 1}]
|
|
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.20 → scrapely_python-0.1.22}/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.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_kvs_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.20 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_rq_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.20 → scrapely_python-0.1.22}/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.20 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.20 → scrapely_python-0.1.22}/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
|