scrapely-python 0.1.12__py3-none-any.whl → 0.1.13__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 CHANGED
@@ -132,7 +132,7 @@ from .errors import (
132
132
  raise_for_status,
133
133
  )
134
134
 
135
- __version__ = '0.1.12'
135
+ __version__ = '0.1.13'
136
136
  __all__ = [
137
137
  # Core
138
138
  'Actor',
scrapely/actor.py CHANGED
@@ -652,12 +652,12 @@ class Actor(metaclass=_ActorMeta):
652
652
 
653
653
  @classmethod
654
654
  def is_at_home(cls) -> bool:
655
- return cls._config.get('is_at_home', False) if cls._config else False
655
+ return bool(cls._env_config().get('is_at_home', False))
656
656
 
657
657
  @classmethod
658
658
  def is_standby(cls) -> bool:
659
659
  """Return True when the actor run was started in standby mode (Node SDK ``isStandby`` parity)."""
660
- return (cls._config.get('meta_origin') == 'STANDBY') if cls._config else False
660
+ return cls._env_config().get('meta_origin') == 'STANDBY'
661
661
 
662
662
  @_ClassProperty
663
663
  def is_paying(cls) -> bool:
@@ -721,7 +721,7 @@ class Actor(metaclass=_ActorMeta):
721
721
  return cast(Dict[str, Any], cls._cached_input)
722
722
 
723
723
  kvs = await cls.open_key_value_store()
724
- input_key = cls._config.get('input_key', 'INPUT') if cls._config else 'INPUT'
724
+ input_key = cls._env_config().get('input_key', 'INPUT')
725
725
  result = await kvs.get_value(input_key)
726
726
  if isinstance(result, dict):
727
727
  result = decrypt_secret_inputs(result)
@@ -848,7 +848,7 @@ class Actor(metaclass=_ActorMeta):
848
848
  name = name_or_id
849
849
 
850
850
  if alias is not None:
851
- storages_json = cls._config.get('actor_storages_json') if cls._config else None
851
+ storages_json = cls._env_config().get('actor_storages_json')
852
852
  resolved = resolve_alias_from_storages_json(alias, storage_type, storages_json)
853
853
  if resolved is not None:
854
854
  id = resolved
@@ -1129,6 +1129,12 @@ class Actor(metaclass=_ActorMeta):
1129
1129
  cls._config = Configuration.get_global_config()
1130
1130
  return cls._config
1131
1131
 
1132
+ @classmethod
1133
+ def _env_config(cls) -> Configuration:
1134
+ # Apify parity: is_at_home() and friends read the environment even
1135
+ # before init(). Doesn't set cls._config, so init() state is unchanged.
1136
+ return cls._config if cls._config is not None else Configuration.get_global_config()
1137
+
1132
1138
  @classmethod
1133
1139
  def new_client(cls, token: Optional[str] = None, base_url: Optional[str] = None) -> ApiClient:
1134
1140
  config = cls.get_config()
scrapely/configuration.py CHANGED
@@ -266,6 +266,22 @@ class Configuration:
266
266
  value = self.get('standby_port')
267
267
  return value if isinstance(value, int) else 4321
268
268
 
269
+ @property
270
+ def web_server_port(self) -> int:
271
+ """Apify name for the container port (ACTOR_WEB_SERVER_PORT, default 4321)."""
272
+ value = self.get('container_port')
273
+ return value if isinstance(value, int) else 4321
274
+
275
+ @property
276
+ def web_server_url(self) -> str:
277
+ """Apify name for the container URL (ACTOR_WEB_SERVER_URL)."""
278
+ return cast(str, self.get('container_url'))
279
+
280
+ @property
281
+ def standby_url(self) -> str:
282
+ """Public standby URL (ACTOR_STANDBY_URL), falling back to the container URL."""
283
+ return cast(str, self.get('standby_url') or self.get('container_url'))
284
+
269
285
  @property
270
286
  def token(self) -> Optional[str]:
271
287
  """Get the authentication token."""
@@ -5,6 +5,7 @@ Base API client for Scrapely storage operations.
5
5
  from __future__ import annotations
6
6
 
7
7
  import asyncio
8
+ import json
8
9
  import logging
9
10
  import os
10
11
  import random
@@ -311,6 +312,10 @@ class ApiClient:
311
312
  request_headers = dict(headers or {})
312
313
  if json_data is not None and 'Content-Type' not in request_headers:
313
314
  request_headers['Content-Type'] = 'application/json'
315
+ if json_data is not None:
316
+ # Apify/crawlee parity: unknown objects (datetimes, LLM result
317
+ # objects, ...) are stored as strings instead of raising.
318
+ content = json.dumps(json_data, ensure_ascii=False, default=str).encode('utf-8')
314
319
 
315
320
  if idempotent is None:
316
321
  idempotent = method.upper() in IDEMPOTENT_METHODS
@@ -343,7 +348,6 @@ class ApiClient:
343
348
  method,
344
349
  url,
345
350
  params=params,
346
- json=json_data,
347
351
  content=content,
348
352
  headers=request_headers or None,
349
353
  **request_kwargs,
@@ -6,6 +6,7 @@ Ports the patterns from the TypeScript ApiStorageClient.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import os
9
10
  import logging
10
11
  import re
11
12
  import uuid
@@ -358,7 +359,14 @@ class ApiStorageClient:
358
359
  return cast(Dict[str, Any], await self.api_client.get_json(f'/v2/key-value-stores/{store_id}/keys', params=params))
359
360
 
360
361
  def kvs_public_url(self, store_id: str, key: str) -> str:
361
- return f'{self.api_client.api_url}{kvs_record_path(store_id, key)}'
362
+ # api_url is the container-internal API address; links handed to users
363
+ # must use the public base URL the platform injects.
364
+ public_base = (
365
+ os.environ.get('APIFY_API_PUBLIC_BASE_URL')
366
+ or os.environ.get('ACTOR_API_PUBLIC_BASE_URL')
367
+ or self.api_client.api_url
368
+ ).rstrip('/')
369
+ return f'{public_base}{kvs_record_path(store_id, key)}'
362
370
 
363
371
  # --- Request queue resource ops ---
364
372
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapely-python
3
- Version: 0.1.12
3
+ Version: 0.1.13
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
@@ -1,11 +1,11 @@
1
- scrapely/__init__.py,sha256=R7A3fQmfkS8LtiN04-32fWvjo6kjNrBSlaeFJCLNgjc,5746
1
+ scrapely/__init__.py,sha256=op7jaJu8R-WYcJhTBg85PBitZyUU99cW8GlZSOgQh2Y,5746
2
2
  scrapely/_browser_locations.py,sha256=8QVVnkms2bDn3_LDYgYDR59MvFx8757l_JYnZ7xb3oI,145269
3
- scrapely/actor.py,sha256=xo25KfueM_quEFrD9buWqIjja69LSt-koCVIdg3arqg,77912
3
+ scrapely/actor.py,sha256=RL--_STBMxpy428v1T2xlI89Poibinl_fBKzcbaHonk,78144
4
4
  scrapely/browser_configuration.py,sha256=jAZapNSHLFW05B96df4wNBtcyiiU9DA3ml0yTL1FwfA,5770
5
5
  scrapely/cdp_retry.py,sha256=hdTJEAIKhApM7IOk1DrQEl8NoflSJSmqd4RbNJ2IsgI,8540
6
6
  scrapely/charging.py,sha256=_GHEKEgQ7lRYTVPacBiBuvlaqGTNcsF65K8xhiJ0dNM,68526
7
7
  scrapely/charging_client.py,sha256=-4q-Ww7gu9EZXpM9QIn1Vd4vwAXmgL_WEKqaQIx3ViA,3961
8
- scrapely/configuration.py,sha256=GjMr3YsxM16HYjAOTUzWZKG2E8FuMzAoFXmkn5dbDPM,16396
8
+ scrapely/configuration.py,sha256=Of-lKm7hoPGUQO8d08kV7fAy2co9QTfb8Of_8ds5PSA,17028
9
9
  scrapely/errors.py,sha256=DCAlVvE3jToZuPEG2HVDL7i8D8hLboomQ5JpSOcsXoQ,4107
10
10
  scrapely/log.py,sha256=iVKoQdMLQXEWz1STzbSXrNUxsS_FNwiIdqoEg7CzeqU,1440
11
11
  scrapely/patch_browser_pool.py,sha256=qZstdQ1rFw8kGjzB9fm3GXco8Jt40eklTw1Czf_vh-8,38018
@@ -23,8 +23,8 @@ scrapely/request_loaders/__init__.py,sha256=aNgr4pRt3MziUbU6UQ_bjTT3moMNvvyUsPeC
23
23
  scrapely/request_loaders/_scrapely_request_list.py,sha256=vguj-EvBjvrGY5_vf2fGXnJBfnhlPeRjdkjPFaxUA7g,7628
24
24
  scrapely/storage/__init__.py,sha256=diAhxEvOXjc3YVhZvzkO3MttJUXQWJ5ogxstiApsh7U,1405
25
25
  scrapely/storage/_metadata.py,sha256=WWawBnVN0b1iK2K-SlxZXUc6czb5wXWf8iC_0tzkgAQ,4764
26
- scrapely/storage/api_client.py,sha256=aWuMwMlHiOkKDCxcN3KDH8Fgf-W2SCSCtphlm80d8nw,17935
27
- scrapely/storage/client.py,sha256=Ln_NA8nJz62nXldkcj54PFZopX5P3Lp8HGot_8friNM,22015
26
+ scrapely/storage/api_client.py,sha256=SRtDOR58fggYwWjcz-S5RshDCM4kO2opxqn_k9Wdyxg,18183
27
+ scrapely/storage/client.py,sha256=qQtFf1G2wv2RU5uHil521j3EAI7JDLX9VngPePe3VMU,22353
28
28
  scrapely/storage/patch_storage.py,sha256=o3Fv0BByeZ1T9HhkzhY6bjolOFsIQcnqBoHq1EdG8Fw,8758
29
29
  scrapely/storage/scrapely_dataset_client.py,sha256=u5dnXxe1BAsq7rh-haR5W1ChE7UZ_k7B9ShR8YtwFkg,10695
30
30
  scrapely/storage/scrapely_kvs_client.py,sha256=ItQhe2rcSl20NkSmegfqHiRyKncH4Qg9BjQVpF6jHfk,7403
@@ -38,10 +38,10 @@ scrapely/storage/_file_system/_key_value_store_client.py,sha256=4Ipgbh4qEqAkwQJ9
38
38
  scrapely/storage/_file_system/_storage_client.py,sha256=aaGJ4P0QAtIgEMl6yHpuobeWFD5M4rMC5HwhOOvuGuo,1794
39
39
  scrapely/storage_clients/__init__.py,sha256=cs1YX7JV2_GKBXeiiJgkQFali08b5_y_jH6Yw7iROAA,656
40
40
  scrapely/storages/__init__.py,sha256=4PAELhJNZ3exJ7Uouicn-bCQ58NafKjhLBixQ8ATQc4,173
41
- scrapely_python-0.1.12.dist-info/licenses/LICENSE,sha256=pQGY0XwjIYh-a2HfawxHq1en53bXkFkDj2Q70jKWHr4,1088
42
- scrapely_python-0.1.12.dist-info/licenses/LICENSE-APACHE,sha256=N8m0FzMq8i4UbTgXZvKEZJSD-27OF8pWZYS_atz4kTU,11355
43
- scrapely_python-0.1.12.dist-info/licenses/NOTICE,sha256=DEsNrw6UdSIqdYYqMWVFgJnyt6jUivTD7iFU6YM_e1A,1122
44
- scrapely_python-0.1.12.dist-info/METADATA,sha256=zVfjqlKUO2cXYOX8JTl6b8d8mjpS6qXocCrY8PDK9qI,3487
45
- scrapely_python-0.1.12.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
46
- scrapely_python-0.1.12.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
47
- scrapely_python-0.1.12.dist-info/RECORD,,
41
+ scrapely_python-0.1.13.dist-info/licenses/LICENSE,sha256=pQGY0XwjIYh-a2HfawxHq1en53bXkFkDj2Q70jKWHr4,1088
42
+ scrapely_python-0.1.13.dist-info/licenses/LICENSE-APACHE,sha256=N8m0FzMq8i4UbTgXZvKEZJSD-27OF8pWZYS_atz4kTU,11355
43
+ scrapely_python-0.1.13.dist-info/licenses/NOTICE,sha256=DEsNrw6UdSIqdYYqMWVFgJnyt6jUivTD7iFU6YM_e1A,1122
44
+ scrapely_python-0.1.13.dist-info/METADATA,sha256=IlnwQFkV2dC5TTZYZjJwaqBvqahQyhtl2RTdfQR3F28,3487
45
+ scrapely_python-0.1.13.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
46
+ scrapely_python-0.1.13.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
47
+ scrapely_python-0.1.13.dist-info/RECORD,,