scrapely-python 0.1.34__tar.gz → 0.1.36__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.34 → scrapely_python-0.1.36}/CHANGELOG.md +11 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/PKG-INFO +1 -1
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/pyproject.toml +1 -1
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/actor.py +44 -5
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/api_client.py +8 -6
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/PKG-INFO +1 -1
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/LICENSE +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/MANIFEST.in +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/NOTICE +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/README.md +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/setup.cfg +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/charging.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/_actor_runner.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/_async_thread.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/_logging_config.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/_serialization.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/_warnings.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/extensions/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/extensions/_graceful_stop.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/extensions/_httpcache.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/middlewares/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/middlewares/apify_proxy.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/pipelines/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/pipelines/actor_dataset_push.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/requests.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/scheduler.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/utils.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/solver.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.36] - 2026-10-26
|
|
8
|
+
|
|
9
|
+
- Opening a named dataset, key-value store or request queue (`Actor.open_dataset('name')` and the like) retries when the API answers `503 storage-name-busy`. The API sends it when another request kept creating or renaming a storage of that name for over 15 s; nothing was changed, so the open is simply repeated. It used to fail the open.
|
|
10
|
+
|
|
11
|
+
## [0.1.35] - 2026-10-26
|
|
12
|
+
|
|
13
|
+
- Once `Actor.fail()` has run, the run stays failed. A later `Actor.exit()` keeps the failure's exit code and status message, so the usual `try: ... await Actor.fail(...) finally: await Actor.exit()` pattern now ends the run as FAILED. Before, the `exit()` in `finally` replaced the failure's exit code 1 with 0 and the run ended as SUCCEEDED with the failure message as its status. The same holds after `fail()` whose `SystemExit` was caught, after `fail(ExitOptions(exit=False))` (also inside `async with Actor(...)` and `Actor.main()`), and for a second `fail()`, which keeps the first failure's code and message.
|
|
14
|
+
- `Actor.fail(ExitOptions(...))` without an exit code exits with 1. `ExitOptions` defaults `exit_code` to 0, which made such a failure exit with 0. A non-zero `exit_code` in the options, or an explicit `exit_code=` keyword, still wins.
|
|
15
|
+
|
|
5
16
|
## [0.1.34] - 2026-10-23
|
|
6
17
|
|
|
7
18
|
- Debug captures (`Actor.debug_capture`, `Actor.debug_capture_sync`) store parsed XML/HTML elements as their markup instead of a repr such as `<Element 'change' at 0x...>`: `xml.etree.ElementTree` elements and trees, lxml elements and trees, BeautifulSoup tags, selectolax nodes and parsel / Scrapy selectors, passed alone or inside a dict or list. The text after an element (its tail) is not included. Other objects are still stored as their `str()`. Captures are only active on the platform's own diagnostic runs (`SCRAPELY_DEBUG_CAPTURE=1`).
|
|
@@ -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.36"
|
|
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"
|
|
@@ -349,6 +349,10 @@ class Actor(metaclass=_ActorMeta):
|
|
|
349
349
|
_cached_input: Optional[Dict[str, Any]] = None
|
|
350
350
|
_exit_code: int = 0
|
|
351
351
|
_status_message: Optional[str] = None
|
|
352
|
+
# Set by fail() for the rest of the lifecycle: (exit code, status message).
|
|
353
|
+
# A later exit() keeps them, so `fail()` followed by the usual `exit()` in a
|
|
354
|
+
# `finally:` block still ends the run as FAILED (it used to exit with 0).
|
|
355
|
+
_failure: Optional[Tuple[int, Optional[str]]] = None
|
|
352
356
|
_charging_manager: Optional[ChargingManager] = None
|
|
353
357
|
_use_state_stores: set = set()
|
|
354
358
|
# Lifecycle bookkeeping set in init() / cleared in _shutdown().
|
|
@@ -392,9 +396,11 @@ class Actor(metaclass=_ActorMeta):
|
|
|
392
396
|
elif exc_type is not None:
|
|
393
397
|
cls._exit_code = 1
|
|
394
398
|
logger.error(f'Actor failed: {exc_val}')
|
|
399
|
+
elif cls._failure is not None:
|
|
400
|
+
cls._exit_code = cls._failure[0] # fail(exit=False) earlier: the run stays failed
|
|
395
401
|
else:
|
|
396
402
|
cls._exit_code = self._instance_exit_code
|
|
397
|
-
if self._instance_status_message:
|
|
403
|
+
if self._instance_status_message and cls._failure is None:
|
|
398
404
|
cls._status_message = self._instance_status_message
|
|
399
405
|
await cls._persist_terminal_status_message(self._instance_status_message)
|
|
400
406
|
await cls._shutdown(timeout_secs=int(self._cleanup_timeout.total_seconds()))
|
|
@@ -410,6 +416,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
410
416
|
cls._main_loop = asyncio.get_running_loop() # for debug_capture_sync from worker threads
|
|
411
417
|
if cls._initialized:
|
|
412
418
|
return
|
|
419
|
+
cls._failure = None # a new lifecycle starts unfailed
|
|
413
420
|
|
|
414
421
|
if options is None or options.configure_logging:
|
|
415
422
|
_lazy('_configure_logging')()
|
|
@@ -701,13 +708,33 @@ class Actor(metaclass=_ActorMeta):
|
|
|
701
708
|
if exit_code is not None:
|
|
702
709
|
opts.exit_code = exit_code
|
|
703
710
|
|
|
704
|
-
if
|
|
711
|
+
if cls._failure is not None:
|
|
712
|
+
# Actor.fail() already ran: the run stays failed. Keep its exit code and
|
|
713
|
+
# status message; a later exit() (e.g. in a `finally:` block) only
|
|
714
|
+
# finishes the shutdown and exits with the failure's code.
|
|
715
|
+
if opts.status_message:
|
|
716
|
+
logger.info(f'[Scrapely] {opts.status_message}')
|
|
717
|
+
logger.info(
|
|
718
|
+
'[Scrapely] Actor.exit() after Actor.fail(): the run stays failed (exit code %s).',
|
|
719
|
+
cls._failure[0],
|
|
720
|
+
)
|
|
721
|
+
opts = ExitOptions(**{**opts.__dict__})
|
|
722
|
+
opts.exit_code, opts.status_message = cls._failure
|
|
723
|
+
await cls._exit_with(opts, persist_message=False)
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
await cls._exit_with(opts)
|
|
727
|
+
|
|
728
|
+
@classmethod
|
|
729
|
+
async def _exit_with(cls, opts: ExitOptions, persist_message: bool = True) -> None:
|
|
730
|
+
if opts.status_message and persist_message:
|
|
705
731
|
logger.info(f'[Scrapely] {opts.status_message}')
|
|
706
732
|
|
|
707
733
|
cls._exit_code = opts.exit_code
|
|
708
734
|
cls._status_message = opts.status_message
|
|
709
735
|
|
|
710
|
-
|
|
736
|
+
if persist_message:
|
|
737
|
+
await cls._persist_terminal_status_message(opts.status_message)
|
|
711
738
|
await cls._shutdown(timeout_secs=opts.timeout_secs)
|
|
712
739
|
|
|
713
740
|
if opts.exit:
|
|
@@ -736,7 +763,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
736
763
|
if isinstance(message_or_options, str):
|
|
737
764
|
opts = ExitOptions(exit_code=1, status_message=message_or_options)
|
|
738
765
|
else:
|
|
739
|
-
# Options-object form:
|
|
766
|
+
# Options-object form: a non-zero exit_code in the object wins over
|
|
740
767
|
# the fail() default of 1 (JS SDK semantics). Build the dict first —
|
|
741
768
|
# passing it alongside exit_code=1 as kwargs raises
|
|
742
769
|
# "got multiple values for keyword argument 'exit_code'".
|
|
@@ -751,12 +778,24 @@ class Actor(metaclass=_ActorMeta):
|
|
|
751
778
|
opts.status_message = status_message
|
|
752
779
|
if exit_code is not None:
|
|
753
780
|
opts.exit_code = exit_code
|
|
781
|
+
elif not opts.exit_code:
|
|
782
|
+
# ExitOptions always carries exit_code (default 0), so 0 from an options
|
|
783
|
+
# object means "not set": a failed run must not exit with 0. Only an
|
|
784
|
+
# explicit exit_code=0 keyword keeps 0.
|
|
785
|
+
opts.exit_code = 1
|
|
754
786
|
|
|
755
787
|
logger.error(
|
|
756
788
|
f'[Scrapely] Actor failed: {opts.status_message or "Unknown error"}',
|
|
757
789
|
exc_info=exception,
|
|
758
790
|
)
|
|
759
|
-
|
|
791
|
+
if cls._failure is not None:
|
|
792
|
+
# Failed already: the first failure's code and message stay.
|
|
793
|
+
opts = ExitOptions(**{**opts.__dict__})
|
|
794
|
+
opts.exit_code, opts.status_message = cls._failure
|
|
795
|
+
await cls._exit_with(opts, persist_message=False)
|
|
796
|
+
return
|
|
797
|
+
cls._failure = (opts.exit_code, opts.status_message)
|
|
798
|
+
await cls._exit_with(opts)
|
|
760
799
|
|
|
761
800
|
@classmethod
|
|
762
801
|
def is_at_home(cls) -> bool:
|
|
@@ -40,11 +40,13 @@ def is_retryable_response(
|
|
|
40
40
|
|
|
41
41
|
429 always: the run API limiter and dataset backpressure refuse it before
|
|
42
42
|
anything is written. Idempotent methods: also 500/502/503/504. Others
|
|
43
|
-
(dataset push, KV PUT) only on a 503 that certainly
|
|
44
|
-
rate limiter refused it ("rate-limit-unavailable",
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
(dataset push, KV PUT, named storage create) only on a 503 that certainly
|
|
44
|
+
was not processed: the rate limiter refused it ("rate-limit-unavailable",
|
|
45
|
+
before the handler), another request held the storage name
|
|
46
|
+
("storage-name-busy", nothing changed), or HAProxy had no backend (its own
|
|
47
|
+
page, nothing forwarded). Any other 503 may mean "outcome unknown" (a
|
|
48
|
+
direct dataset write whose commit was not confirmed), and re-sending could
|
|
49
|
+
store the items twice.
|
|
48
50
|
"""
|
|
49
51
|
if status == 429:
|
|
50
52
|
return True
|
|
@@ -52,7 +54,7 @@ def is_retryable_response(
|
|
|
52
54
|
return status in (500, 502, 503, 504)
|
|
53
55
|
if status != 503:
|
|
54
56
|
return False
|
|
55
|
-
return error_type
|
|
57
|
+
return error_type in ('rate-limit-unavailable', 'storage-name-busy') or NO_BACKEND_MARKER in (body_text or '')
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def unwrap_data(value: Any) -> Any:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/extensions/__init__.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/extensions/_graceful_stop.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/extensions/_httpcache.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/middlewares/__init__.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/scrapy/middlewares/apify_proxy.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
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/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
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_kvs_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely/storage/scrapely_rq_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/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.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.34 → scrapely_python-0.1.36}/src/scrapely_python.egg-info/top_level.txt
RENAMED
|
File without changes
|