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.
Files changed (71) hide show
  1. {scrapely_python-0.1.21/src/scrapely_python.egg-info → scrapely_python-0.1.22}/PKG-INFO +1 -1
  2. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/pyproject.toml +1 -1
  3. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/__init__.py +1 -1
  4. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/actor.py +18 -4
  5. {scrapely_python-0.1.21 → scrapely_python-0.1.22/src/scrapely_python.egg-info}/PKG-INFO +1 -1
  6. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charge_limit_guard.py +95 -0
  7. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/LICENSE +0 -0
  8. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/LICENSE-APACHE +0 -0
  9. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/NOTICE +0 -0
  10. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/README.md +0 -0
  11. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/setup.cfg +0 -0
  12. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/_browser_locations.py +0 -0
  13. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/browser_configuration.py +0 -0
  14. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/cdp_retry.py +0 -0
  15. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/charging.py +0 -0
  16. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/charging_client.py +0 -0
  17. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/configuration.py +0 -0
  18. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/crypto/__init__.py +0 -0
  19. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/crypto/_secrets.py +0 -0
  20. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/errors.py +0 -0
  21. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/events/__init__.py +0 -0
  22. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/events/_types.py +0 -0
  23. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/events/event_manager.py +0 -0
  24. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/log.py +0 -0
  25. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/patch_browser_pool.py +0 -0
  26. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/pricing_types.py +0 -0
  27. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/proxy_configuration.py +0 -0
  28. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/py.typed +0 -0
  29. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/request_loaders/__init__.py +0 -0
  30. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
  31. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/solver.py +0 -0
  32. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/__init__.py +0 -0
  33. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/__init__.py +0 -0
  34. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
  35. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
  36. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/_metadata.py +0 -0
  37. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/api_client.py +0 -0
  38. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/client.py +0 -0
  39. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/patch_storage.py +0 -0
  40. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
  41. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
  42. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_rq_client.py +0 -0
  43. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/scrapely_storage_client.py +0 -0
  44. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
  45. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/types.py +0 -0
  46. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage/utils.py +0 -0
  47. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storage_clients/__init__.py +0 -0
  48. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/storages/__init__.py +0 -0
  49. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/types.py +0 -0
  50. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely/webhook.py +0 -0
  51. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
  52. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
  53. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/requires.txt +0 -0
  54. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/src/scrapely_python.egg-info/top_level.txt +0 -0
  55. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_api_client_retries.py +0 -0
  56. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_apify_parity.py +0 -0
  57. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_browser_configuration.py +0 -0
  58. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_cdp_retry.py +0 -0
  59. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charge_retries.py +0 -0
  60. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charging_parity.py +0 -0
  61. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_charging_pending.py +0 -0
  62. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_input_proxy_aliases.py +0 -0
  63. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_parity.py +0 -0
  64. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_patch_browser_pool.py +0 -0
  65. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_proxy_configuration.py +0 -0
  66. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_push_idempotency.py +0 -0
  67. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_run_control.py +0 -0
  68. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_secrets.py +0 -0
  69. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_solver.py +0 -0
  70. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_storage.py +0 -0
  71. {scrapely_python-0.1.21 → scrapely_python-0.1.22}/tests/test_terminal_status_message.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.21
3
+ Version: 0.1.22
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.21"
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"
@@ -132,7 +132,7 @@ from .errors import (
132
132
  raise_for_status,
133
133
  )
134
134
 
135
- __version__ = '0.1.21'
135
+ __version__ = '0.1.22'
136
136
  __all__ = [
137
137
  # Core
138
138
  'Actor',
@@ -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
- True, # Actor.push_data always targets the default dataset
840
- cls._default_dataset.push_data,
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
- True, # Actor.push_data always targets the default dataset
850
- cls._default_dataset.push_data,
863
+ is_default,
864
+ target.push_data,
851
865
  )
852
866
 
853
867
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.21
3
+ Version: 0.1.22
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
@@ -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}]