scrapely-python 0.1.30__tar.gz → 0.1.31__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.30 → scrapely_python-0.1.31}/CHANGELOG.md +4 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/PKG-INFO +1 -1
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/pyproject.toml +1 -1
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/actor.py +4 -4
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/client.py +30 -7
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/PKG-INFO +1 -1
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/LICENSE +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/MANIFEST.in +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/NOTICE +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/README.md +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/setup.cfg +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/charging.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/solver.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.31] - 2026-10-20
|
|
6
|
+
|
|
7
|
+
- The storage client percent-encodes request ids and key-value store record keys as one URL path segment and refuses `''`, `.` and `..`. A request id such as `..` read from a queue used to send the call to another resource.
|
|
8
|
+
|
|
5
9
|
## [0.1.30] - 2026-10-20
|
|
6
10
|
|
|
7
11
|
- Removed `apify_margin_percentage` from the pricing info models; the API no longer returns it.
|
|
@@ -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.31"
|
|
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"
|
|
@@ -1756,10 +1756,10 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1756
1756
|
|
|
1757
1757
|
opts = options or MetamorphOptions()
|
|
1758
1758
|
|
|
1759
|
-
# Wire format (server contract): the
|
|
1760
|
-
#
|
|
1761
|
-
#
|
|
1762
|
-
#
|
|
1759
|
+
# Wire format (server contract): the new input (if any) goes in the
|
|
1760
|
+
# request body's `input` field; the server stores it as
|
|
1761
|
+
# INPUT-METAMORPH-<n> (INPUT keeps the original) and tracks the
|
|
1762
|
+
# metamorph count in run stats. targetActorId/build are accepted in the body.
|
|
1763
1763
|
client = cls.new_client()
|
|
1764
1764
|
body: Dict[str, Any] = {'targetActorId': target_actor_id}
|
|
1765
1765
|
if opts.build:
|
|
@@ -47,9 +47,32 @@ def is_storage_uuid(value: str) -> bool:
|
|
|
47
47
|
return bool(STORAGE_UUID_REGEX.match(value))
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def path_segment(value: str) -> str:
|
|
51
|
+
"""Percent-encode a value so it stays one URL path segment.
|
|
52
|
+
|
|
53
|
+
`''`, `.` and `..` are rejected: a URL parser resolves dot segments even when
|
|
54
|
+
percent-encoded, so the call would land on the parent resource.
|
|
55
|
+
"""
|
|
56
|
+
if not isinstance(value, str):
|
|
57
|
+
raise TypeError(f'Expected a string for a URL path segment, got {type(value).__name__}')
|
|
58
|
+
if value in ('', '.', '..'):
|
|
59
|
+
raise ValueError(f'"{value}" cannot be used as a URL path segment.')
|
|
60
|
+
return quote(value, safe='')
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
def kvs_record_path(store_id: str, key: str) -> str:
|
|
51
64
|
"""Build a KVS record path with a URL-encoded key."""
|
|
52
|
-
return f'/v2/key-value-stores/{store_id}/records/{
|
|
65
|
+
return f'/v2/key-value-stores/{store_id}/records/{path_segment(key)}'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def rq_request_path(queue_id: str, request_id: str, suffix: str = '') -> str:
|
|
69
|
+
"""Build the path of one queued request with the id URL-encoded.
|
|
70
|
+
|
|
71
|
+
Request ids come from queue contents, which another client may have written:
|
|
72
|
+
unencoded, an id such as `..` or `../../key-value-stores/x` would send the
|
|
73
|
+
call to another resource.
|
|
74
|
+
"""
|
|
75
|
+
return f'/v2/request-queues/{queue_id}/requests/{path_segment(request_id)}{suffix}'
|
|
53
76
|
|
|
54
77
|
|
|
55
78
|
def build_rq_params(
|
|
@@ -478,7 +501,7 @@ class ApiStorageClient:
|
|
|
478
501
|
|
|
479
502
|
async def rq_get_request(self, queue_id: str, request_id: str) -> Optional[Request]:
|
|
480
503
|
try:
|
|
481
|
-
data = await self.api_client.get_json(
|
|
504
|
+
data = await self.api_client.get_json(rq_request_path(queue_id, request_id))
|
|
482
505
|
return Request.from_dict(data)
|
|
483
506
|
except ApiClientError as err:
|
|
484
507
|
if err.status_code == 404:
|
|
@@ -523,7 +546,7 @@ class ApiStorageClient:
|
|
|
523
546
|
payload = request.to_dict()
|
|
524
547
|
payload['handledAt'] = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
|
|
525
548
|
data = await self.api_client.put_json(
|
|
526
|
-
|
|
549
|
+
rq_request_path(queue_id, request.id),
|
|
527
550
|
payload,
|
|
528
551
|
params=build_rq_params(client_key),
|
|
529
552
|
)
|
|
@@ -561,12 +584,12 @@ class ApiStorageClient:
|
|
|
561
584
|
# Persist retryCount/errorMessages before unlocking, so the next fetch
|
|
562
585
|
# sees them and max_request_retries is ever reached.
|
|
563
586
|
await self.api_client.put_json(
|
|
564
|
-
|
|
587
|
+
rq_request_path(queue_id, request.id),
|
|
565
588
|
request.to_dict(),
|
|
566
589
|
params=build_rq_params(client_key, forefront=forefront),
|
|
567
590
|
)
|
|
568
591
|
await self.api_client.delete(
|
|
569
|
-
|
|
592
|
+
rq_request_path(queue_id, request.id, '/lock'),
|
|
570
593
|
params=build_rq_params(client_key, forefront=forefront),
|
|
571
594
|
)
|
|
572
595
|
return ProcessedRequest(
|
|
@@ -601,7 +624,7 @@ class ApiStorageClient:
|
|
|
601
624
|
)
|
|
602
625
|
|
|
603
626
|
async def rq_delete_request(self, queue_id: str, request_id: str) -> None:
|
|
604
|
-
await self.api_client.delete(
|
|
627
|
+
await self.api_client.delete(rq_request_path(queue_id, request_id))
|
|
605
628
|
|
|
606
629
|
async def rq_prolong_request_lock(
|
|
607
630
|
self,
|
|
@@ -614,7 +637,7 @@ class ApiStorageClient:
|
|
|
614
637
|
) -> Dict[str, Any]:
|
|
615
638
|
"""Prolong the lock on a request (REST API: PUT /requests/{id}/lock)."""
|
|
616
639
|
data = await self.api_client.put_json(
|
|
617
|
-
|
|
640
|
+
rq_request_path(queue_id, request_id, '/lock'),
|
|
618
641
|
params=build_rq_params(client_key, lock_secs=lock_secs, forefront=forefront),
|
|
619
642
|
)
|
|
620
643
|
return {'lockExpiresAt': data.get('lockExpiresAt')}
|
|
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.30 → scrapely_python-0.1.31}/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.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_kvs_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_rq_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.30 → scrapely_python-0.1.31}/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.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/top_level.txt
RENAMED
|
File without changes
|