scrapely-python 0.1.1__py3-none-any.whl → 0.1.3__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/actor.py +9 -8
- scrapely/charging.py +132 -1
- scrapely/proxy_configuration.py +38 -9
- {scrapely_python-0.1.1.dist-info → scrapely_python-0.1.3.dist-info}/METADATA +3 -4
- {scrapely_python-0.1.1.dist-info → scrapely_python-0.1.3.dist-info}/RECORD +8 -8
- {scrapely_python-0.1.1.dist-info → scrapely_python-0.1.3.dist-info}/WHEEL +0 -0
- {scrapely_python-0.1.1.dist-info → scrapely_python-0.1.3.dist-info}/top_level.txt +0 -0
scrapely/__init__.py
CHANGED
scrapely/actor.py
CHANGED
|
@@ -1776,19 +1776,20 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1776
1776
|
"""
|
|
1777
1777
|
cls._ensure_initialized('create_proxy_configuration')
|
|
1778
1778
|
|
|
1779
|
-
from .proxy_configuration import ProxyConfiguration
|
|
1779
|
+
from .proxy_configuration import ProxyConfiguration, _input_proxy_option, _input_use_platform_proxy
|
|
1780
1780
|
|
|
1781
1781
|
# Handle actor_proxy_input (Node SDK normalizeProxyInput/createProxyConfiguration
|
|
1782
|
-
# parity): `
|
|
1782
|
+
# parity): `useScrapelyProxy: false` (or the legacy `useApifyProxy: false`; a
|
|
1783
|
+
# boolean useScrapelyProxy wins) explicitly disables the PLATFORM proxy only —
|
|
1783
1784
|
# custom proxies (proxyUrls / tiered lists / new_url_function) from the same
|
|
1784
1785
|
# input must still be used. The standard "Custom proxy URLs" input shape is
|
|
1785
|
-
# {
|
|
1786
|
+
# {useScrapelyProxy: false, proxyUrls: [...]}; returning None for it would
|
|
1786
1787
|
# silently drop the user's own proxies and crawl on their real IPs. We only
|
|
1787
1788
|
# bail when no custom proxies are given either. A MISSING key falls back to
|
|
1788
1789
|
# platform mode when the server injected credentials — input like
|
|
1789
|
-
# {
|
|
1790
|
+
# {scrapelyProxyGroups: [...]} must not silently disable proxying.
|
|
1790
1791
|
if actor_proxy_input is not None:
|
|
1791
|
-
if actor_proxy_input
|
|
1792
|
+
if _input_use_platform_proxy(actor_proxy_input) is False and not (
|
|
1792
1793
|
proxy_urls
|
|
1793
1794
|
or new_url_function
|
|
1794
1795
|
or tiered_proxy_urls
|
|
@@ -1798,9 +1799,9 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1798
1799
|
or actor_proxy_input.get('tieredProxyConfig')
|
|
1799
1800
|
):
|
|
1800
1801
|
return None
|
|
1801
|
-
country_code = country_code or actor_proxy_input
|
|
1802
|
-
subdivision_code = subdivision_code or actor_proxy_input
|
|
1803
|
-
groups = groups or actor_proxy_input
|
|
1802
|
+
country_code = country_code or _input_proxy_option(actor_proxy_input, 'Country')
|
|
1803
|
+
subdivision_code = subdivision_code or _input_proxy_option(actor_proxy_input, 'Subdivision')
|
|
1804
|
+
groups = groups or _input_proxy_option(actor_proxy_input, 'Groups')
|
|
1804
1805
|
proxy_urls = proxy_urls or actor_proxy_input.get('proxyUrls')
|
|
1805
1806
|
tiered_proxy_urls = tiered_proxy_urls or actor_proxy_input.get('tieredProxyUrls')
|
|
1806
1807
|
tiered_proxy_config = tiered_proxy_config or actor_proxy_input.get('tieredProxyConfig')
|
scrapely/charging.py
CHANGED
|
@@ -195,6 +195,11 @@ class ChargingManager:
|
|
|
195
195
|
self._pending_charge_keys: Dict[str, Dict[str, Any]] = {}
|
|
196
196
|
# Optional local charging-log dataset (ACTOR_USE_CHARGING_LOG_DATASET).
|
|
197
197
|
self._charging_log_dataset: Any = None
|
|
198
|
+
# Idempotency key of the last charge the platform accepted
|
|
199
|
+
# (push_data_and_charge voids it when the push then fails).
|
|
200
|
+
self._last_accepted_operation_id: Optional[str] = None
|
|
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]] = []
|
|
198
203
|
|
|
199
204
|
def close(self) -> None:
|
|
200
205
|
"""Mark the manager closed; subsequent charge/push operations are rejected."""
|
|
@@ -429,6 +434,50 @@ class ChargingManager:
|
|
|
429
434
|
self._total_charged_usd - event_price * Decimal(count),
|
|
430
435
|
)
|
|
431
436
|
|
|
437
|
+
def _take_last_accepted_operation_id(self) -> Optional[str]:
|
|
438
|
+
op_id = self._last_accepted_operation_id
|
|
439
|
+
self._last_accepted_operation_id = None
|
|
440
|
+
return op_id
|
|
441
|
+
|
|
442
|
+
async def _void_charge(self, event_name: str, operation_id: str, reserved: int) -> bool:
|
|
443
|
+
"""Reverse an accepted charge whose items were never stored, then unwind locally.
|
|
444
|
+
|
|
445
|
+
POST /v2/actor-runs/{run}/charge/void with the charge's Idempotency-Key.
|
|
446
|
+
The local reservation is released only once the platform confirms.
|
|
447
|
+
"""
|
|
448
|
+
if self._client is None or not self._is_at_home or not self._actor_run_id:
|
|
449
|
+
return False
|
|
450
|
+
for attempt in range(self._POST_ATTEMPTS):
|
|
451
|
+
try:
|
|
452
|
+
await self._client.post(
|
|
453
|
+
f'/v2/actor-runs/{self._actor_run_id}/charge/void',
|
|
454
|
+
json_data={},
|
|
455
|
+
headers={'Idempotency-Key': operation_id},
|
|
456
|
+
)
|
|
457
|
+
self._unwind_reservation(event_name, reserved)
|
|
458
|
+
return True
|
|
459
|
+
except Exception as err: # noqa: BLE001
|
|
460
|
+
status = _http_status_of(err)
|
|
461
|
+
# 404: nothing to void; 409: already settled; 4xx auth: permanent.
|
|
462
|
+
if status is not None and 400 <= status < 500:
|
|
463
|
+
return False
|
|
464
|
+
if attempt + 1 < self._POST_ATTEMPTS:
|
|
465
|
+
await asyncio.sleep(0.25 * (attempt + 1))
|
|
466
|
+
# Outcome unknown: keep the reservation and retry before the next push / at exit.
|
|
467
|
+
self._pending_voids.append((event_name, operation_id, reserved))
|
|
468
|
+
logger.error('Void of charge %s (%r) not confirmed yet; will retry', operation_id, event_name)
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
async def _retry_pending_voids(self) -> bool:
|
|
472
|
+
"""Retry unconfirmed voids (caller holds the operation lock).
|
|
473
|
+
|
|
474
|
+
Returns False while any earlier void is still unconfirmed.
|
|
475
|
+
"""
|
|
476
|
+
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)
|
|
479
|
+
return not self._pending_voids
|
|
480
|
+
|
|
432
481
|
def _quarantine_stale_pending(self) -> None:
|
|
433
482
|
"""Remove intents older than the safe replay window (fail closed, no replay).
|
|
434
483
|
|
|
@@ -544,6 +593,7 @@ class ChargingManager:
|
|
|
544
593
|
"""Caller must hold _operation_lock."""
|
|
545
594
|
if not self._is_at_home or self._client is None or not self._actor_run_id:
|
|
546
595
|
return 0
|
|
596
|
+
await self._retry_pending_voids()
|
|
547
597
|
|
|
548
598
|
import time
|
|
549
599
|
|
|
@@ -731,6 +781,8 @@ class ChargingManager:
|
|
|
731
781
|
)
|
|
732
782
|
if accepted is not None:
|
|
733
783
|
self._drop_pending(idempotency_key)
|
|
784
|
+
if accepted > 0:
|
|
785
|
+
self._last_accepted_operation_id = idempotency_key
|
|
734
786
|
if accepted < chargeable_count:
|
|
735
787
|
self._unwind_reservation(event_name, chargeable_count - accepted)
|
|
736
788
|
chargeable_count = accepted
|
|
@@ -1032,6 +1084,9 @@ async def push_data_and_charge(
|
|
|
1032
1084
|
async with charging_manager._operation_lock:
|
|
1033
1085
|
if charging_manager._closed:
|
|
1034
1086
|
raise RuntimeError('ChargingManager is closed (actor is shutting down); charge rejected')
|
|
1087
|
+
# A void that did not go through for an earlier failed push must land
|
|
1088
|
+
# before this push charges again, or one unsaved result is billed twice.
|
|
1089
|
+
voids_confirmed = await charging_manager._retry_pending_voids()
|
|
1035
1090
|
# Non-PPE actors push everything without charging (JS calculatePushDataLimits
|
|
1036
1091
|
# parity: `if (!this.isPayPerEvent) return { limitedItems, eventsToCharge: {} }`).
|
|
1037
1092
|
is_ppe = charging_manager._pricing_info is not None
|
|
@@ -1065,6 +1120,29 @@ async def push_data_and_charge(
|
|
|
1065
1120
|
event_charge_limit_reached=len(items) > 0 and limited_count == 0,
|
|
1066
1121
|
)
|
|
1067
1122
|
|
|
1123
|
+
# Every platform event this push would charge counts: the explicit event
|
|
1124
|
+
# and, on a default-dataset push, the configured default event. The
|
|
1125
|
+
# synthetic default-dataset event (local bookkeeping) and unpriced
|
|
1126
|
+
# events (never posted) do not.
|
|
1127
|
+
charged_names: List[str] = []
|
|
1128
|
+
if event_name:
|
|
1129
|
+
charged_names.append(event_name)
|
|
1130
|
+
if is_default_dataset:
|
|
1131
|
+
configured_default = charging_manager._config.get('default_dataset_item_event_name')
|
|
1132
|
+
if configured_default:
|
|
1133
|
+
charged_names.append(configured_default)
|
|
1134
|
+
if (
|
|
1135
|
+
not voids_confirmed
|
|
1136
|
+
and is_ppe
|
|
1137
|
+
and any(
|
|
1138
|
+
not is_default_dataset_item_event(name) and name in charging_manager._per_event_prices
|
|
1139
|
+
for name in charged_names
|
|
1140
|
+
)
|
|
1141
|
+
):
|
|
1142
|
+
raise RuntimeError(
|
|
1143
|
+
'An earlier charge for a failed dataset push could not be voided yet; retry the push'
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1068
1146
|
# Charge BEFORE persisting so a server cap/reject cannot leave unbilled
|
|
1069
1147
|
# items in the dataset. Operation lock is held across both steps.
|
|
1070
1148
|
if is_ppe and resolved_event:
|
|
@@ -1079,15 +1157,20 @@ async def push_data_and_charge(
|
|
|
1079
1157
|
|
|
1080
1158
|
# Synthetic storage event first (local-only); then explicit billable event.
|
|
1081
1159
|
reserved_in_op: list[tuple[str, int]] = []
|
|
1160
|
+
operation_ids: list[Optional[str]] = []
|
|
1082
1161
|
try:
|
|
1083
1162
|
if default_event:
|
|
1163
|
+
charging_manager._take_last_accepted_operation_id()
|
|
1084
1164
|
r = await charging_manager._charge_exclusive(default_event, len(limited_items))
|
|
1085
1165
|
results.append(r)
|
|
1086
1166
|
reserved_in_op.append((default_event, r.charged_count))
|
|
1167
|
+
operation_ids.append(charging_manager._take_last_accepted_operation_id())
|
|
1087
1168
|
if event_name and event_name != default_event:
|
|
1169
|
+
charging_manager._take_last_accepted_operation_id()
|
|
1088
1170
|
r = await charging_manager._charge_exclusive(event_name, len(limited_items))
|
|
1089
1171
|
results.append(r)
|
|
1090
1172
|
reserved_in_op.append((event_name, r.charged_count))
|
|
1173
|
+
operation_ids.append(charging_manager._take_last_accepted_operation_id())
|
|
1091
1174
|
except Exception:
|
|
1092
1175
|
for name, reserved in reserved_in_op:
|
|
1093
1176
|
if reserved > 0:
|
|
@@ -1118,11 +1201,27 @@ async def push_data_and_charge(
|
|
|
1118
1201
|
try:
|
|
1119
1202
|
await push_fn(limited_items[:accepted_for_items])
|
|
1120
1203
|
except Exception as push_err:
|
|
1204
|
+
# A result is billed only if it was saved: void this
|
|
1205
|
+
# operation's platform charges and unwind local
|
|
1206
|
+
# reservations, then let the caller retry the push.
|
|
1121
1207
|
logger.error(
|
|
1122
|
-
'Dataset push failed after charge accepted (items=%s): %s',
|
|
1208
|
+
'Dataset push failed after charge accepted (items=%s): %s; voiding the charge',
|
|
1123
1209
|
accepted_for_items,
|
|
1124
1210
|
push_err,
|
|
1125
1211
|
)
|
|
1212
|
+
# Only when the push certainly stored nothing (the storage API
|
|
1213
|
+
# answered with an error, or the request was never sent). A
|
|
1214
|
+
# timeout after sending may have stored the items.
|
|
1215
|
+
if not _push_certainly_failed(push_err):
|
|
1216
|
+
raise
|
|
1217
|
+
for (name, _), op_id in zip(reserved_in_op, operation_ids):
|
|
1218
|
+
kept = min(accepted_for_items, next(r.charged_count for n, r in charged_events if n == name))
|
|
1219
|
+
if kept <= 0:
|
|
1220
|
+
continue
|
|
1221
|
+
if op_id:
|
|
1222
|
+
await charging_manager._void_charge(name, op_id, kept)
|
|
1223
|
+
elif is_default_dataset_item_event(name):
|
|
1224
|
+
charging_manager._unwind_reservation(name, kept)
|
|
1126
1225
|
raise
|
|
1127
1226
|
return ChargeResult(
|
|
1128
1227
|
chargeable_within_limit=charging_manager.compute_chargeable(),
|
|
@@ -1141,3 +1240,35 @@ async def push_data_and_charge(
|
|
|
1141
1240
|
charged_count=0,
|
|
1142
1241
|
event_charge_limit_reached=False,
|
|
1143
1242
|
)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _http_status_of(err: BaseException) -> Optional[int]:
|
|
1246
|
+
status = getattr(err, 'status_code', None)
|
|
1247
|
+
if status is None:
|
|
1248
|
+
response = getattr(err, 'response', None)
|
|
1249
|
+
status = getattr(response, 'status_code', None) if response is not None else None
|
|
1250
|
+
try:
|
|
1251
|
+
return int(status) if status is not None else None
|
|
1252
|
+
except (TypeError, ValueError):
|
|
1253
|
+
return None
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def _push_certainly_failed(err: BaseException) -> bool:
|
|
1257
|
+
"""True when a dataset push certainly stored nothing.
|
|
1258
|
+
|
|
1259
|
+
The storage API answered with an error status, or the connection was never
|
|
1260
|
+
made. Timeouts and resets after sending are not proof: the write may have landed.
|
|
1261
|
+
"""
|
|
1262
|
+
import httpx
|
|
1263
|
+
|
|
1264
|
+
e: Optional[BaseException] = err
|
|
1265
|
+
for _ in range(4):
|
|
1266
|
+
if e is None:
|
|
1267
|
+
break
|
|
1268
|
+
status = _http_status_of(e)
|
|
1269
|
+
if status is not None:
|
|
1270
|
+
return status >= 400
|
|
1271
|
+
if isinstance(e, (httpx.ConnectError, ConnectionRefusedError)):
|
|
1272
|
+
return True
|
|
1273
|
+
e = e.__cause__ or e.__context__
|
|
1274
|
+
return False
|
scrapely/proxy_configuration.py
CHANGED
|
@@ -108,6 +108,25 @@ def _sanitize_fragment(value: str) -> str:
|
|
|
108
108
|
return re.sub(r'[:@/?#\s]', '', value)
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def _input_use_platform_proxy(actor_proxy_input: Dict[str, Any]) -> Optional[bool]:
|
|
112
|
+
"""The proxy input field's platform-proxy switch, or None when it has none.
|
|
113
|
+
|
|
114
|
+
`useScrapelyProxy` wins when it is a boolean; otherwise the legacy
|
|
115
|
+
`useApifyProxy` key of existing inputs and ported actors decides.
|
|
116
|
+
"""
|
|
117
|
+
for key in ('useScrapelyProxy', 'useApifyProxy'):
|
|
118
|
+
value = actor_proxy_input.get(key)
|
|
119
|
+
if isinstance(value, bool):
|
|
120
|
+
return value
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _input_proxy_option(actor_proxy_input: Dict[str, Any], name: str) -> Any:
|
|
125
|
+
"""`scrapelyProxy<name>` of the proxy input field when it is set, else the legacy `apifyProxy<name>`."""
|
|
126
|
+
value = actor_proxy_input.get(f'scrapelyProxy{name}')
|
|
127
|
+
return value if value is not None else actor_proxy_input.get(f'apifyProxy{name}')
|
|
128
|
+
|
|
129
|
+
|
|
111
130
|
def _platform_env() -> Optional[Dict[str, Any]]:
|
|
112
131
|
"""Read platform proxy credentials from the environment (or None)."""
|
|
113
132
|
hostname = os.environ.get('SCRAPELY_PROXY_HOSTNAME') or os.environ.get('APIFY_PROXY_HOSTNAME')
|
|
@@ -213,10 +232,10 @@ class ProxyConfiguration:
|
|
|
213
232
|
|
|
214
233
|
# If actor_proxy_input is provided, extract settings from it
|
|
215
234
|
if actor_proxy_input:
|
|
216
|
-
if actor_proxy_input
|
|
217
|
-
self._groups = actor_proxy_input
|
|
218
|
-
self._country_code = actor_proxy_input
|
|
219
|
-
self._subdivision_code = actor_proxy_input
|
|
235
|
+
if _input_use_platform_proxy(actor_proxy_input):
|
|
236
|
+
self._groups = _input_proxy_option(actor_proxy_input, 'Groups') or []
|
|
237
|
+
self._country_code = _input_proxy_option(actor_proxy_input, 'Country')
|
|
238
|
+
self._subdivision_code = _input_proxy_option(actor_proxy_input, 'Subdivision')
|
|
220
239
|
else:
|
|
221
240
|
self._proxy_urls = actor_proxy_input.get('proxyUrls', []) or []
|
|
222
241
|
# Tiered lists may also arrive through actor input (future-proof
|
|
@@ -291,7 +310,7 @@ class ProxyConfiguration:
|
|
|
291
310
|
)
|
|
292
311
|
if has_real_custom and (self._groups or self._country_code or self._subdivision_code or password):
|
|
293
312
|
raise ValueError(
|
|
294
|
-
'Cannot combine custom proxies with
|
|
313
|
+
'Cannot combine custom proxies with Scrapely Proxy! It is not allowed to set '
|
|
295
314
|
'"proxy_urls" or "new_url_function" combined with "groups", "country_code" '
|
|
296
315
|
'or "subdivision_code".'
|
|
297
316
|
)
|
|
@@ -480,7 +499,8 @@ class ProxyConfiguration:
|
|
|
480
499
|
Sends ``GET {status_url}/?format=json`` *through the platform gateway``
|
|
481
500
|
so the proxy handshake itself authenticates the run's credentials.
|
|
482
501
|
Mirrors the JS SDK: 4s timeout, 2 attempts, returns ``None`` when the
|
|
483
|
-
endpoint stays unreachable. Never raises.
|
|
502
|
+
endpoint stays unreachable. Never raises. ``fromPayload`` is True when
|
|
503
|
+
the gateway answered with its own status JSON rather than an HTTP error.
|
|
484
504
|
"""
|
|
485
505
|
import urllib.error
|
|
486
506
|
import urllib.request
|
|
@@ -514,6 +534,7 @@ class ProxyConfiguration:
|
|
|
514
534
|
'connected': payload.get('connected') is True,
|
|
515
535
|
'connectionError': payload.get('connectionError') or '',
|
|
516
536
|
'isManInTheMiddle': payload.get('isManInTheMiddle') is True,
|
|
537
|
+
'fromPayload': True,
|
|
517
538
|
}
|
|
518
539
|
except urllib.error.HTTPError as e:
|
|
519
540
|
if e.code == 407:
|
|
@@ -524,6 +545,7 @@ class ProxyConfiguration:
|
|
|
524
545
|
'SCRAPELY_PROXY_*/APIFY_PROXY_* credentials.'
|
|
525
546
|
),
|
|
526
547
|
'isManInTheMiddle': False,
|
|
548
|
+
'fromPayload': False,
|
|
527
549
|
}
|
|
528
550
|
if 590 <= e.code <= 599:
|
|
529
551
|
description = PROXY_STATUS_CODE_DESCRIPTIONS.get(e.code, 'upstream error')
|
|
@@ -531,6 +553,7 @@ class ProxyConfiguration:
|
|
|
531
553
|
'connected': False,
|
|
532
554
|
'connectionError': f'Platform proxy upstream error (HTTP {e.code}: {description}).',
|
|
533
555
|
'isManInTheMiddle': False,
|
|
556
|
+
'fromPayload': False,
|
|
534
557
|
}
|
|
535
558
|
# Other HTTP errors behave like connection errors: retry.
|
|
536
559
|
except Exception: # noqa: BLE001 - retry connection errors (Apify behavior)
|
|
@@ -623,6 +646,9 @@ class ProxyConfiguration:
|
|
|
623
646
|
|
|
624
647
|
Raises:
|
|
625
648
|
ValueError: If proxy configuration is invalid
|
|
649
|
+
ConnectionError: If the platform proxy reports that this configuration
|
|
650
|
+
cannot be used (for example an unknown proxy group), with the
|
|
651
|
+
proxy's ``connectionError`` as the message (Apify parity)
|
|
626
652
|
"""
|
|
627
653
|
if self._initialized:
|
|
628
654
|
return
|
|
@@ -639,9 +665,10 @@ class ProxyConfiguration:
|
|
|
639
665
|
if self._proxy_urls and self._new_url_function:
|
|
640
666
|
raise ValueError('Cannot specify both proxy_urls and new_url_function')
|
|
641
667
|
|
|
642
|
-
# Apify-parity access check
|
|
643
|
-
#
|
|
644
|
-
#
|
|
668
|
+
# Apify-parity access check: GET {status_url}/?format=json *through the
|
|
669
|
+
# platform gateway* so the proxy handshake itself authenticates the run's
|
|
670
|
+
# credentials. A "connected": false answer raises like Apify's
|
|
671
|
+
# _check_access; timeouts and HTTP errors only warn. Parity with the
|
|
645
672
|
# Apify SDKs (usesApifyProxy / uses_apify_proxy) and the JS Scrapely
|
|
646
673
|
# port (platformEnv): the check only applies when this configuration
|
|
647
674
|
# actually uses the platform proxy — BYOP-only configurations skip it
|
|
@@ -662,6 +689,8 @@ class ProxyConfiguration:
|
|
|
662
689
|
)
|
|
663
690
|
else:
|
|
664
691
|
self._is_man_in_the_middle = status['isManInTheMiddle']
|
|
692
|
+
if not status['connected'] and status['fromPayload']:
|
|
693
|
+
raise ConnectionError(status['connectionError'])
|
|
665
694
|
if not status['connected']:
|
|
666
695
|
logger.warning('Platform proxy access check failed: %s', status['connectionError'])
|
|
667
696
|
else:
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapely-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform
|
|
5
5
|
Author-email: Scrapely <support@scrape.ly>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://scrape.ly
|
|
8
8
|
Project-URL: Documentation, https://docs.scrape.ly
|
|
9
9
|
Project-URL: Repository, https://github.com/SandeepDev1/scrapely
|
|
10
10
|
Keywords: scraping,actor,serverless,apify,scrapely
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -22,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
22
21
|
Requires-Dist: httpx>=0.25.0
|
|
23
22
|
Requires-Dist: crawlee<2.0.0,>=1.8.0
|
|
24
23
|
Requires-Dist: websockets>=12.0
|
|
25
|
-
Requires-Dist: scrapely-client-python<2,>=1.0.
|
|
24
|
+
Requires-Dist: scrapely-client-python<2,>=1.0.2
|
|
26
25
|
Requires-Dist: pydantic>=2.0.0
|
|
27
26
|
Requires-Dist: cryptography>=42.0.0
|
|
28
27
|
Provides-Extra: dev
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
scrapely/__init__.py,sha256=
|
|
1
|
+
scrapely/__init__.py,sha256=vJx9b5JOO4ehkpvB03Xjoj7dCIzw4lgLggG-0jWrkM0,5745
|
|
2
2
|
scrapely/_browser_locations.py,sha256=8QVVnkms2bDn3_LDYgYDR59MvFx8757l_JYnZ7xb3oI,145269
|
|
3
|
-
scrapely/actor.py,sha256=
|
|
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=eoOWnbtUPcfuh96Ss6XZmOveLInprs6EjSAtrGrCd9A,53364
|
|
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
|
|
9
9
|
scrapely/log.py,sha256=iVKoQdMLQXEWz1STzbSXrNUxsS_FNwiIdqoEg7CzeqU,1440
|
|
10
10
|
scrapely/patch_browser_pool.py,sha256=sRAjuzKoOEgn0X8695c_9qioRALzkTWXoDlorVnK9jg,37426
|
|
11
11
|
scrapely/pricing_types.py,sha256=J6wCddZd-U1MYeOtaaEdeHSsp1HBgiQLfvSInBOvhIo,5305
|
|
12
|
-
scrapely/proxy_configuration.py,sha256=
|
|
12
|
+
scrapely/proxy_configuration.py,sha256=rSCXJFuUEIK31chk05FVLRQKeVA34h_pBRbr8IKNXo0,36195
|
|
13
13
|
scrapely/py.typed,sha256=XtWlywqUjc6x_M2gAXlDtYXbAsYkCjKZEjFyJvaHvm4,105
|
|
14
14
|
scrapely/types.py,sha256=ceVuHvhYbPai2CL3vFmXLTMoGaMR27IefJFgWhzd7ws,8900
|
|
15
15
|
scrapely/webhook.py,sha256=4WOM42ypOSOs4lKtP5ofxAh6lX-7c8p5U9lOfuFzx-g,2830
|
|
@@ -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.3.dist-info/METADATA,sha256=pp3DbPQObzUEt1NXKP6prb8mCreU6crymE1QG2-EFSM,3104
|
|
41
|
+
scrapely_python-0.1.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
42
|
+
scrapely_python-0.1.3.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
|
|
43
|
+
scrapely_python-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|