scrapely-python 0.1.18__py3-none-any.whl → 0.1.19__py3-none-any.whl

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/__init__.py CHANGED
@@ -132,7 +132,7 @@ from .errors import (
132
132
  raise_for_status,
133
133
  )
134
134
 
135
- __version__ = '0.1.18'
135
+ __version__ = '0.1.19'
136
136
  __all__ = [
137
137
  # Core
138
138
  'Actor',
scrapely/charging.py CHANGED
@@ -240,6 +240,12 @@ class ChargingManager:
240
240
  self._charged_events: Dict[str, int] = {}
241
241
  self._max_total_charge_usd: Optional[Decimal] = None
242
242
  self._total_charged_usd: Decimal = Decimal('0.0')
243
+ # Run spend the platform counts against maxTotalChargeUsd that this SDK
244
+ # does not charge itself: compute/proxy/browser usage and server-billed
245
+ # events. Learnt from the charge endpoint's remainingBudgetNd (only ever
246
+ # grows), so the local budget gates on the run's true remaining budget.
247
+ self._platform_other_usd: Decimal = Decimal('0')
248
+ self._last_remaining_budget_nd: Optional[int] = None
243
249
  self._charge_lock = RLock()
244
250
  self._flush_lock = asyncio.Lock()
245
251
  # Serializes budget calculation/reservation with PPE-aware dataset pushes.
@@ -288,6 +294,27 @@ class ChargingManager:
288
294
  self._refused_default_dataset_items,
289
295
  )
290
296
 
297
+ def _remaining_budget_usd(self) -> Decimal:
298
+ """maxTotalChargeUsd minus this SDK's charges and the platform's other run spend."""
299
+ assert self._max_total_charge_usd is not None
300
+ return self._max_total_charge_usd - self._total_charged_usd - self._platform_other_usd
301
+
302
+ def _sync_platform_budget(self) -> None:
303
+ """Fold the last charge response's remainingBudgetNd into the local budget.
304
+
305
+ Called after the local reservation matches what the platform accepted.
306
+ The platform's remaining budget = max - (our charges + usage + server
307
+ events), so ``other`` = max - remaining - our charges. Monotonic: usage
308
+ never shrinks, and a stale answer must not hand budget back.
309
+ """
310
+ nd = self._last_remaining_budget_nd
311
+ self._last_remaining_budget_nd = None
312
+ if nd is None or self._max_total_charge_usd is None or nd < 0:
313
+ return
314
+ other = self._max_total_charge_usd - Decimal(nd) / Decimal(1_000_000_000) - self._total_charged_usd
315
+ if other > self._platform_other_usd:
316
+ self._platform_other_usd = other
317
+
291
318
  def is_charge_limit_reached(self) -> bool:
292
319
  """Whether this pay-per-event run has used up its spending limit.
293
320
 
@@ -307,7 +334,7 @@ class ChargingManager:
307
334
  prices = [price for price in self._per_event_prices.values() if price > 0]
308
335
  if not prices:
309
336
  return False
310
- return self._max_total_charge_usd - self._total_charged_usd < min(prices)
337
+ return self._remaining_budget_usd() < min(prices)
311
338
 
312
339
  def _mark_charge_limit_hit(self) -> None:
313
340
  if self._pricing_info is not None:
@@ -795,6 +822,7 @@ class ChargingManager:
795
822
  idempotency_key=idempotency_key,
796
823
  deadline=deadline,
797
824
  )
825
+ self._last_remaining_budget_nd = _read_remaining_budget_nd(body)
798
826
  return _read_accepted_count(body, count)
799
827
  except Exception as e:
800
828
  last_error = e
@@ -957,7 +985,7 @@ class ChargingManager:
957
985
  # legacy one-event overdraw while the budget is not yet exceeded.
958
986
  chargeable_count = count
959
987
  if self._max_total_charge_usd is not None and affordability_price > 0:
960
- remaining = self._max_total_charge_usd - self._total_charged_usd
988
+ remaining = self._remaining_budget_usd()
961
989
  max_count_by_limit = max(0, int(remaining / affordability_price))
962
990
  if count > max_count_by_limit:
963
991
  chargeable_count = (
@@ -1045,6 +1073,15 @@ class ChargingManager:
1045
1073
  charged_count=chargeable_count,
1046
1074
  event_charge_limit_reached=self._is_event_limit_reached(event_name),
1047
1075
  )
1076
+ self._sync_platform_budget()
1077
+ if self._max_total_charge_usd is not None and self._platform_other_usd > 0:
1078
+ # Usage may have used up what is left: report it on this result.
1079
+ result = ChargeResult(
1080
+ chargeable_within_limit=self._compute_chargeable(),
1081
+ charged_count=result.charged_count,
1082
+ event_charge_limit_reached=result.event_charge_limit_reached
1083
+ or self._is_event_limit_reached(event_name),
1084
+ )
1048
1085
  else:
1049
1086
  raise ChargeUncertainError(
1050
1087
  f'Charge for event {event_name!r} (count={chargeable_count}) '
@@ -1103,7 +1140,7 @@ class ChargingManager:
1103
1140
  if event_price is None or event_price <= 0:
1104
1141
  return None
1105
1142
 
1106
- remaining = self._max_total_charge_usd - self._total_charged_usd
1143
+ remaining = self._remaining_budget_usd()
1107
1144
  return max(0, int(remaining / event_price))
1108
1145
 
1109
1146
  def calculate_total_charged_amount(self) -> Decimal:
@@ -1165,7 +1202,7 @@ class ChargingManager:
1165
1202
  if self._max_total_charge_usd is None:
1166
1203
  return items_count
1167
1204
 
1168
- remaining = self._max_total_charge_usd - self._total_charged_usd
1205
+ remaining = self._remaining_budget_usd()
1169
1206
  max_items = int(remaining / combined_price)
1170
1207
 
1171
1208
  return min(items_count, max(0, max_items))
@@ -1226,7 +1263,7 @@ class ChargingManager:
1226
1263
  if self._max_total_charge_usd is None:
1227
1264
  return False
1228
1265
 
1229
- remaining = self._max_total_charge_usd - self._total_charged_usd
1266
+ remaining = self._remaining_budget_usd()
1230
1267
  return remaining < event_price
1231
1268
 
1232
1269
  def _compute_chargeable(self) -> Dict[str, Optional[int]]:
@@ -1264,6 +1301,17 @@ def _is_charge_limit_rejection(err: ChargeRejectedError) -> bool:
1264
1301
  )
1265
1302
 
1266
1303
 
1304
+ def _read_remaining_budget_nd(body: Any) -> Optional[int]:
1305
+ """remainingBudgetNd from a charge response (platforms that send it), else None."""
1306
+ if not isinstance(body, dict):
1307
+ return None
1308
+ data = body.get('data') if isinstance(body.get('data'), dict) else body
1309
+ raw = data.get('remainingBudgetNd') if isinstance(data, dict) else None
1310
+ if isinstance(raw, (int, float)) and not isinstance(raw, bool) and raw >= 0:
1311
+ return int(raw)
1312
+ return None
1313
+
1314
+
1267
1315
  def _read_accepted_count(body: Any, fallback: int) -> int:
1268
1316
  if not isinstance(body, dict):
1269
1317
  return fallback
@@ -1653,7 +1701,7 @@ async def push_data_with_charges(
1653
1701
  )
1654
1702
  rows = len(rows_in)
1655
1703
  if charging_manager._max_total_charge_usd is not None and row_cost > 0:
1656
- remaining = charging_manager._max_total_charge_usd - charging_manager._total_charged_usd
1704
+ remaining = charging_manager._remaining_budget_usd()
1657
1705
  rows = min(rows, max(0, int(remaining / row_cost)))
1658
1706
 
1659
1707
  ops: List[tuple] = [] # (event, charged, op_id, uncertain)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.18
3
+ Version: 0.1.19
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
@@ -1,9 +1,9 @@
1
- scrapely/__init__.py,sha256=EHGVITjobhdZu3nhkmBmECrlftUMCsnDyLAIxdnZ_7A,5746
1
+ scrapely/__init__.py,sha256=7AUWixuhrohpSFpDkEHo809IBKI-2u-m4ZWPMorYKSk,5746
2
2
  scrapely/_browser_locations.py,sha256=8QVVnkms2bDn3_LDYgYDR59MvFx8757l_JYnZ7xb3oI,145269
3
3
  scrapely/actor.py,sha256=Lsb4_14RCGQy-EWACnqijo7Hz5xvPKSDZcu4B8_ZrHc,81685
4
4
  scrapely/browser_configuration.py,sha256=jAZapNSHLFW05B96df4wNBtcyiiU9DA3ml0yTL1FwfA,5770
5
5
  scrapely/cdp_retry.py,sha256=hdTJEAIKhApM7IOk1DrQEl8NoflSJSmqd4RbNJ2IsgI,8540
6
- scrapely/charging.py,sha256=maRgijO8vM8mCb_S9lnvF99zNR_G8sB3LsubU1r6t6M,79326
6
+ scrapely/charging.py,sha256=TxcALWN4hhwJ0i2raYmtHGA1P79lQ1bmhBMtrvNACIw,81862
7
7
  scrapely/charging_client.py,sha256=-4q-Ww7gu9EZXpM9QIn1Vd4vwAXmgL_WEKqaQIx3ViA,3961
8
8
  scrapely/configuration.py,sha256=Of-lKm7hoPGUQO8d08kV7fAy2co9QTfb8Of_8ds5PSA,17028
9
9
  scrapely/errors.py,sha256=DCAlVvE3jToZuPEG2HVDL7i8D8hLboomQ5JpSOcsXoQ,4107
@@ -39,10 +39,10 @@ scrapely/storage/_file_system/_key_value_store_client.py,sha256=4Ipgbh4qEqAkwQJ9
39
39
  scrapely/storage/_file_system/_storage_client.py,sha256=aaGJ4P0QAtIgEMl6yHpuobeWFD5M4rMC5HwhOOvuGuo,1794
40
40
  scrapely/storage_clients/__init__.py,sha256=cs1YX7JV2_GKBXeiiJgkQFali08b5_y_jH6Yw7iROAA,656
41
41
  scrapely/storages/__init__.py,sha256=4PAELhJNZ3exJ7Uouicn-bCQ58NafKjhLBixQ8ATQc4,173
42
- scrapely_python-0.1.18.dist-info/licenses/LICENSE,sha256=pQGY0XwjIYh-a2HfawxHq1en53bXkFkDj2Q70jKWHr4,1088
43
- scrapely_python-0.1.18.dist-info/licenses/LICENSE-APACHE,sha256=N8m0FzMq8i4UbTgXZvKEZJSD-27OF8pWZYS_atz4kTU,11355
44
- scrapely_python-0.1.18.dist-info/licenses/NOTICE,sha256=DEsNrw6UdSIqdYYqMWVFgJnyt6jUivTD7iFU6YM_e1A,1122
45
- scrapely_python-0.1.18.dist-info/METADATA,sha256=3znh1pGrKvP0jNRAftMgiGSKuBG1c3g8UWJtgMYI95g,3487
46
- scrapely_python-0.1.18.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
47
- scrapely_python-0.1.18.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
48
- scrapely_python-0.1.18.dist-info/RECORD,,
42
+ scrapely_python-0.1.19.dist-info/licenses/LICENSE,sha256=pQGY0XwjIYh-a2HfawxHq1en53bXkFkDj2Q70jKWHr4,1088
43
+ scrapely_python-0.1.19.dist-info/licenses/LICENSE-APACHE,sha256=N8m0FzMq8i4UbTgXZvKEZJSD-27OF8pWZYS_atz4kTU,11355
44
+ scrapely_python-0.1.19.dist-info/licenses/NOTICE,sha256=DEsNrw6UdSIqdYYqMWVFgJnyt6jUivTD7iFU6YM_e1A,1122
45
+ scrapely_python-0.1.19.dist-info/METADATA,sha256=PzFQ4BxF0FgSjgOvSQYimfc5v6Pq2notGIWYHCBa-iQ,3487
46
+ scrapely_python-0.1.19.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
47
+ scrapely_python-0.1.19.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
48
+ scrapely_python-0.1.19.dist-info/RECORD,,