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.
Files changed (55) hide show
  1. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/CHANGELOG.md +4 -0
  2. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/PKG-INFO +1 -1
  3. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/pyproject.toml +1 -1
  4. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/__init__.py +1 -1
  5. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/actor.py +4 -4
  6. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/client.py +30 -7
  7. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/PKG-INFO +1 -1
  8. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/LICENSE +0 -0
  9. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/LICENSE-APACHE +0 -0
  10. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/MANIFEST.in +0 -0
  11. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/NOTICE +0 -0
  12. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/README.md +0 -0
  13. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/setup.cfg +0 -0
  14. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/_browser_locations.py +0 -0
  15. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/browser_configuration.py +0 -0
  16. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/cdp_retry.py +0 -0
  17. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/charging.py +0 -0
  18. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/charging_client.py +0 -0
  19. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/configuration.py +0 -0
  20. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/crypto/__init__.py +0 -0
  21. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/crypto/_secrets.py +0 -0
  22. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/errors.py +0 -0
  23. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/events/__init__.py +0 -0
  24. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/events/_types.py +0 -0
  25. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/events/event_manager.py +0 -0
  26. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/log.py +0 -0
  27. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/patch_browser_pool.py +0 -0
  28. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/pricing_types.py +0 -0
  29. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/proxy_configuration.py +0 -0
  30. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/py.typed +0 -0
  31. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/request_loaders/__init__.py +0 -0
  32. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
  33. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/solver.py +0 -0
  34. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/__init__.py +0 -0
  35. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_file_system/__init__.py +0 -0
  36. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
  37. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
  38. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/_metadata.py +0 -0
  39. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/api_client.py +0 -0
  40. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/patch_storage.py +0 -0
  41. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
  42. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
  43. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_rq_client.py +0 -0
  44. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/scrapely_storage_client.py +0 -0
  45. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
  46. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/types.py +0 -0
  47. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage/utils.py +0 -0
  48. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storage_clients/__init__.py +0 -0
  49. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/storages/__init__.py +0 -0
  50. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/types.py +0 -0
  51. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely/webhook.py +0 -0
  52. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
  53. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
  54. {scrapely_python-0.1.30 → scrapely_python-0.1.31}/src/scrapely_python.egg-info/requires.txt +0 -0
  55. {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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.30
3
+ Version: 0.1.31
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scrapely-python"
7
- version = "0.1.30"
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"
@@ -227,7 +227,7 @@ if TYPE_CHECKING: # Static analysers and IDEs see the lazy names as plain impor
227
227
  set_api_storage,
228
228
  )
229
229
 
230
- __version__ = '0.1.30'
230
+ __version__ = '0.1.31'
231
231
  __all__ = [
232
232
  # Core
233
233
  'Actor',
@@ -1756,10 +1756,10 @@ class Actor(metaclass=_ActorMeta):
1756
1756
 
1757
1757
  opts = options or MetamorphOptions()
1758
1758
 
1759
- # Wire format (server contract): the server copies the current INPUT to
1760
- # INPUT-METAMORPH-<n> itself and tracks the metamorph count in run
1761
- # stats; the new input (if any) is read from the request body's
1762
- # `input` field. targetActorId/build are accepted in the body.
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/{quote(key, safe="")}'
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(f'/v2/request-queues/{queue_id}/requests/{request_id}')
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
- f'/v2/request-queues/{queue_id}/requests/{request.id}',
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
- f'/v2/request-queues/{queue_id}/requests/{request.id}',
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
- f'/v2/request-queues/{queue_id}/requests/{request.id}/lock',
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(f'/v2/request-queues/{queue_id}/requests/{request_id}')
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
- f'/v2/request-queues/{queue_id}/requests/{request_id}/lock',
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')}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.30
3
+ Version: 0.1.31
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