scrapely-python 0.1.3__py3-none-any.whl → 0.1.4__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 +1 -1
- scrapely/charging.py +76 -16
- {scrapely_python-0.1.3.dist-info → scrapely_python-0.1.4.dist-info}/METADATA +1 -1
- {scrapely_python-0.1.3.dist-info → scrapely_python-0.1.4.dist-info}/RECORD +6 -6
- {scrapely_python-0.1.3.dist-info → scrapely_python-0.1.4.dist-info}/WHEEL +0 -0
- {scrapely_python-0.1.3.dist-info → scrapely_python-0.1.4.dist-info}/top_level.txt +0 -0
scrapely/__init__.py
CHANGED
scrapely/charging.py
CHANGED
|
@@ -199,7 +199,7 @@ class ChargingManager:
|
|
|
199
199
|
# (push_data_and_charge voids it when the push then fails).
|
|
200
200
|
self._last_accepted_operation_id: Optional[str] = None
|
|
201
201
|
# Voids not yet confirmed by the platform; retried before the next push and at exit.
|
|
202
|
-
self._pending_voids: List[tuple[str, str, int]] = []
|
|
202
|
+
self._pending_voids: List[tuple[str, str, int, bool]] = []
|
|
203
203
|
|
|
204
204
|
def close(self) -> None:
|
|
205
205
|
"""Mark the manager closed; subsequent charge/push operations are rejected."""
|
|
@@ -439,7 +439,18 @@ class ChargingManager:
|
|
|
439
439
|
self._last_accepted_operation_id = None
|
|
440
440
|
return op_id
|
|
441
441
|
|
|
442
|
-
async def
|
|
442
|
+
async def _cancel_uncertain_charge(self, event_name: str, operation_id: str, reserved: int) -> bool:
|
|
443
|
+
"""Cancel an unconfirmed charge for items that were then not stored.
|
|
444
|
+
|
|
445
|
+
Drops the pending intent (so the exit flush never posts it) and voids it in
|
|
446
|
+
case the original request did apply; a 404 means it never applied.
|
|
447
|
+
"""
|
|
448
|
+
self._drop_pending(operation_id)
|
|
449
|
+
return await self._void_charge(event_name, operation_id, reserved, not_found_means_voided=True)
|
|
450
|
+
|
|
451
|
+
async def _void_charge(
|
|
452
|
+
self, event_name: str, operation_id: str, reserved: int, not_found_means_voided: bool = False
|
|
453
|
+
) -> bool:
|
|
443
454
|
"""Reverse an accepted charge whose items were never stored, then unwind locally.
|
|
444
455
|
|
|
445
456
|
POST /v2/actor-runs/{run}/charge/void with the charge's Idempotency-Key.
|
|
@@ -458,13 +469,19 @@ class ChargingManager:
|
|
|
458
469
|
return True
|
|
459
470
|
except Exception as err: # noqa: BLE001
|
|
460
471
|
status = _http_status_of(err)
|
|
472
|
+
# Only the void route's own answer proves it never applied; a server
|
|
473
|
+
# without the route (older deployment) also answers 404.
|
|
474
|
+
if status == 404 and not_found_means_voided and getattr(err, 'type', None) == 'charge-not-found':
|
|
475
|
+
# The unconfirmed charge never reached the ledger: nothing billed.
|
|
476
|
+
self._unwind_reservation(event_name, reserved)
|
|
477
|
+
return True
|
|
461
478
|
# 404: nothing to void; 409: already settled; 4xx auth: permanent.
|
|
462
479
|
if status is not None and 400 <= status < 500:
|
|
463
480
|
return False
|
|
464
481
|
if attempt + 1 < self._POST_ATTEMPTS:
|
|
465
482
|
await asyncio.sleep(0.25 * (attempt + 1))
|
|
466
483
|
# Outcome unknown: keep the reservation and retry before the next push / at exit.
|
|
467
|
-
self._pending_voids.append((event_name, operation_id, reserved))
|
|
484
|
+
self._pending_voids.append((event_name, operation_id, reserved, not_found_means_voided))
|
|
468
485
|
logger.error('Void of charge %s (%r) not confirmed yet; will retry', operation_id, event_name)
|
|
469
486
|
return False
|
|
470
487
|
|
|
@@ -474,8 +491,8 @@ class ChargingManager:
|
|
|
474
491
|
Returns False while any earlier void is still unconfirmed.
|
|
475
492
|
"""
|
|
476
493
|
pending, self._pending_voids = self._pending_voids, []
|
|
477
|
-
for event_name, operation_id, reserved in pending:
|
|
478
|
-
await self._void_charge(event_name, operation_id, reserved)
|
|
494
|
+
for event_name, operation_id, reserved, not_found_means_voided in pending:
|
|
495
|
+
await self._void_charge(event_name, operation_id, reserved, not_found_means_voided)
|
|
479
496
|
return not self._pending_voids
|
|
480
497
|
|
|
481
498
|
def _quarantine_stale_pending(self) -> None:
|
|
@@ -1158,19 +1175,39 @@ async def push_data_and_charge(
|
|
|
1158
1175
|
# Synthetic storage event first (local-only); then explicit billable event.
|
|
1159
1176
|
reserved_in_op: list[tuple[str, int]] = []
|
|
1160
1177
|
operation_ids: list[Optional[str]] = []
|
|
1178
|
+
uncertain_ops: set[str] = set()
|
|
1179
|
+
|
|
1180
|
+
async def charge_one(name: str) -> None:
|
|
1181
|
+
charging_manager._take_last_accepted_operation_id()
|
|
1182
|
+
try:
|
|
1183
|
+
r = await charging_manager._charge_exclusive(name, len(limited_items))
|
|
1184
|
+
op_id = charging_manager._take_last_accepted_operation_id()
|
|
1185
|
+
except ChargeUncertainError as unconfirmed:
|
|
1186
|
+
# The platform did not confirm the charge (5xx / timeouts after
|
|
1187
|
+
# retries). Never drop the items for that: the intent stays
|
|
1188
|
+
# pending with its idempotency key and is re-posted by the exit
|
|
1189
|
+
# flush, so the items are stored now and billed once.
|
|
1190
|
+
logger.warning(
|
|
1191
|
+
'Charge for %r not confirmed yet (%s); storing the items and retrying the charge later',
|
|
1192
|
+
name,
|
|
1193
|
+
unconfirmed.operation_id,
|
|
1194
|
+
)
|
|
1195
|
+
r = ChargeResult(
|
|
1196
|
+
chargeable_within_limit=charging_manager.compute_chargeable(),
|
|
1197
|
+
charged_count=unconfirmed.count,
|
|
1198
|
+
event_charge_limit_reached=False,
|
|
1199
|
+
)
|
|
1200
|
+
op_id = unconfirmed.operation_id
|
|
1201
|
+
uncertain_ops.add(op_id)
|
|
1202
|
+
results.append(r)
|
|
1203
|
+
reserved_in_op.append((name, r.charged_count))
|
|
1204
|
+
operation_ids.append(op_id)
|
|
1205
|
+
|
|
1161
1206
|
try:
|
|
1162
1207
|
if default_event:
|
|
1163
|
-
|
|
1164
|
-
r = await charging_manager._charge_exclusive(default_event, len(limited_items))
|
|
1165
|
-
results.append(r)
|
|
1166
|
-
reserved_in_op.append((default_event, r.charged_count))
|
|
1167
|
-
operation_ids.append(charging_manager._take_last_accepted_operation_id())
|
|
1208
|
+
await charge_one(default_event)
|
|
1168
1209
|
if event_name and event_name != default_event:
|
|
1169
|
-
|
|
1170
|
-
r = await charging_manager._charge_exclusive(event_name, len(limited_items))
|
|
1171
|
-
results.append(r)
|
|
1172
|
-
reserved_in_op.append((event_name, r.charged_count))
|
|
1173
|
-
operation_ids.append(charging_manager._take_last_accepted_operation_id())
|
|
1210
|
+
await charge_one(event_name)
|
|
1174
1211
|
except Exception:
|
|
1175
1212
|
for name, reserved in reserved_in_op:
|
|
1176
1213
|
if reserved > 0:
|
|
@@ -1192,11 +1229,31 @@ async def push_data_and_charge(
|
|
|
1192
1229
|
)
|
|
1193
1230
|
# Unwind any event that reserved more than we will persist
|
|
1194
1231
|
# (synthetic default-dataset is local-only and otherwise sticks).
|
|
1232
|
+
platform_op_names = {
|
|
1233
|
+
name for (name, _), op_id in zip(reserved_in_op, operation_ids) if op_id
|
|
1234
|
+
}
|
|
1195
1235
|
for name, result in charged_events:
|
|
1196
1236
|
if result.charged_count > accepted_for_items:
|
|
1237
|
+
# Nothing stored and a platform charge was made: the void
|
|
1238
|
+
# below releases its local count once the platform confirms.
|
|
1239
|
+
if accepted_for_items <= 0 and name in platform_op_names:
|
|
1240
|
+
continue
|
|
1197
1241
|
charging_manager._unwind_reservation(
|
|
1198
1242
|
name, result.charged_count - accepted_for_items
|
|
1199
1243
|
)
|
|
1244
|
+
if accepted_for_items <= 0:
|
|
1245
|
+
# Nothing will be stored (another event of this push was capped
|
|
1246
|
+
# to zero): undo every platform charge this operation made, so
|
|
1247
|
+
# no result is billed for items that are never saved. Local
|
|
1248
|
+
# counts were unwound just above.
|
|
1249
|
+
for (name, reserved), op_id in zip(reserved_in_op, operation_ids):
|
|
1250
|
+
if reserved <= 0 or not op_id:
|
|
1251
|
+
continue
|
|
1252
|
+
# Original count: released only when the platform confirms.
|
|
1253
|
+
if op_id in uncertain_ops:
|
|
1254
|
+
await charging_manager._cancel_uncertain_charge(name, op_id, reserved)
|
|
1255
|
+
else:
|
|
1256
|
+
await charging_manager._void_charge(name, op_id, reserved)
|
|
1200
1257
|
if accepted_for_items > 0:
|
|
1201
1258
|
try:
|
|
1202
1259
|
await push_fn(limited_items[:accepted_for_items])
|
|
@@ -1218,7 +1275,10 @@ async def push_data_and_charge(
|
|
|
1218
1275
|
kept = min(accepted_for_items, next(r.charged_count for n, r in charged_events if n == name))
|
|
1219
1276
|
if kept <= 0:
|
|
1220
1277
|
continue
|
|
1221
|
-
if op_id:
|
|
1278
|
+
if op_id and op_id in uncertain_ops:
|
|
1279
|
+
# Never re-post a charge for items that were not stored.
|
|
1280
|
+
await charging_manager._cancel_uncertain_charge(name, op_id, kept)
|
|
1281
|
+
elif op_id:
|
|
1222
1282
|
await charging_manager._void_charge(name, op_id, kept)
|
|
1223
1283
|
elif is_default_dataset_item_event(name):
|
|
1224
1284
|
charging_manager._unwind_reservation(name, kept)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
scrapely/__init__.py,sha256=
|
|
1
|
+
scrapely/__init__.py,sha256=nVwtGU9e9kfzoiX6MdfbK4U-p88J7eMDUkqNkpKY1Ug,5745
|
|
2
2
|
scrapely/_browser_locations.py,sha256=8QVVnkms2bDn3_LDYgYDR59MvFx8757l_JYnZ7xb3oI,145269
|
|
3
3
|
scrapely/actor.py,sha256=QczHOj2GVQnMbQZ_76vjjJNHxPrinWB_WCdZ6_41l00,75126
|
|
4
4
|
scrapely/browser_configuration.py,sha256=jAZapNSHLFW05B96df4wNBtcyiiU9DA3ml0yTL1FwfA,5770
|
|
5
|
-
scrapely/charging.py,sha256=
|
|
5
|
+
scrapely/charging.py,sha256=MFz_Z4qJ-N3qLffleQb2BxjomygM3UMvHSBw1HabG80,56857
|
|
6
6
|
scrapely/charging_client.py,sha256=tDtlPwLSVFvoRa2NBgNjyfHkAUM77SSIyYT2ACgpDm8,3710
|
|
7
7
|
scrapely/configuration.py,sha256=YUnbft9JeWlVTgWalr-CgJCiyJ_e_rgoQItVPvjeWRs,15406
|
|
8
8
|
scrapely/errors.py,sha256=DCAlVvE3jToZuPEG2HVDL7i8D8hLboomQ5JpSOcsXoQ,4107
|
|
@@ -37,7 +37,7 @@ scrapely/storage/_file_system/_key_value_store_client.py,sha256=4Ipgbh4qEqAkwQJ9
|
|
|
37
37
|
scrapely/storage/_file_system/_storage_client.py,sha256=aaGJ4P0QAtIgEMl6yHpuobeWFD5M4rMC5HwhOOvuGuo,1794
|
|
38
38
|
scrapely/storage_clients/__init__.py,sha256=cs1YX7JV2_GKBXeiiJgkQFali08b5_y_jH6Yw7iROAA,656
|
|
39
39
|
scrapely/storages/__init__.py,sha256=4PAELhJNZ3exJ7Uouicn-bCQ58NafKjhLBixQ8ATQc4,173
|
|
40
|
-
scrapely_python-0.1.
|
|
41
|
-
scrapely_python-0.1.
|
|
42
|
-
scrapely_python-0.1.
|
|
43
|
-
scrapely_python-0.1.
|
|
40
|
+
scrapely_python-0.1.4.dist-info/METADATA,sha256=ACM2A5RD1zJ8JV3cEY0jZigW3dpvsjQyiwJBFvKlLv4,3104
|
|
41
|
+
scrapely_python-0.1.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
42
|
+
scrapely_python-0.1.4.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
|
|
43
|
+
scrapely_python-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|