scrapely-python 0.1.21__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.21/src/scrapely_python.egg-info → scrapely_python-0.1.22}/PKG-INFO +1 -1
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/pyproject.toml +1 -1
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/actor.py +18 -4
- {scrapely_python-0.1.21 → scrapely_python-0.1.22/src/scrapely_python.egg-info}/PKG-INFO +1 -1
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charge_limit_guard.py +95 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/LICENSE +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/NOTICE +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/README.md +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/setup.cfg +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/charging.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/solver.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/top_level.txt +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_api_client_retries.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_apify_parity.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_browser_configuration.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_cdp_retry.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charge_retries.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charging_parity.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charging_pending.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_input_proxy_aliases.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_parity.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_patch_browser_pool.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_proxy_configuration.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_push_idempotency.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_run_control.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_secrets.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_solver.py +0 -0
- {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_storage.py +0 -0
- {scrapely_python-0.1.21 → 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"
|
|
@@ -784,6 +784,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
784
784
|
event_name: Optional[str] = None,
|
|
785
785
|
charges: Optional[Dict[str, int]] = None,
|
|
786
786
|
charge_per: str = 'row',
|
|
787
|
+
dataset: Optional[Union[str, Dataset]] = None,
|
|
787
788
|
) -> ChargeResult:
|
|
788
789
|
"""Push data to the default dataset, charging pay-per-event items.
|
|
789
790
|
|
|
@@ -807,6 +808,12 @@ class Actor(metaclass=_ActorMeta):
|
|
|
807
808
|
charges them once for the whole push, e.g.
|
|
808
809
|
``push_data(page_rows, charges={'document': 1}, charge_per='batch')``:
|
|
809
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.
|
|
810
817
|
|
|
811
818
|
Returns:
|
|
812
819
|
ChargeResult describing how many items were charged and pushed.
|
|
@@ -827,6 +834,13 @@ class Actor(metaclass=_ActorMeta):
|
|
|
827
834
|
if cls._default_dataset is None:
|
|
828
835
|
cls._default_dataset = await cls.open_dataset()
|
|
829
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
|
+
|
|
830
844
|
items = data if isinstance(data, list) else [data]
|
|
831
845
|
charging_manager = cls.get_charging_manager()
|
|
832
846
|
if charges is not None:
|
|
@@ -836,8 +850,8 @@ class Actor(metaclass=_ActorMeta):
|
|
|
836
850
|
charging_manager,
|
|
837
851
|
items,
|
|
838
852
|
charges,
|
|
839
|
-
|
|
840
|
-
|
|
853
|
+
is_default,
|
|
854
|
+
target.push_data,
|
|
841
855
|
charge_per=charge_per,
|
|
842
856
|
)
|
|
843
857
|
if charge_per != 'row':
|
|
@@ -846,8 +860,8 @@ class Actor(metaclass=_ActorMeta):
|
|
|
846
860
|
charging_manager,
|
|
847
861
|
items,
|
|
848
862
|
event_name,
|
|
849
|
-
|
|
850
|
-
|
|
863
|
+
is_default,
|
|
864
|
+
target.push_data,
|
|
851
865
|
)
|
|
852
866
|
|
|
853
867
|
@classmethod
|
|
@@ -728,3 +728,98 @@ def test_charge_per_validation() -> None:
|
|
|
728
728
|
api, mgr = _doc('1')
|
|
729
729
|
with pytest.raises(ValueError):
|
|
730
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
|
|
File without changes
|
{scrapely_python-0.1.21 → 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.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_kvs_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_rq_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.21 → 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.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.21 → 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
|