scrapely-python 0.1.0__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.
Files changed (43) hide show
  1. scrapely/__init__.py +246 -0
  2. scrapely/_browser_locations.py +5470 -0
  3. scrapely/actor.py +1836 -0
  4. scrapely/browser_configuration.py +146 -0
  5. scrapely/charging.py +1143 -0
  6. scrapely/charging_client.py +107 -0
  7. scrapely/configuration.py +375 -0
  8. scrapely/crypto/__init__.py +21 -0
  9. scrapely/crypto/_secrets.py +210 -0
  10. scrapely/errors.py +153 -0
  11. scrapely/events/__init__.py +57 -0
  12. scrapely/events/_types.py +194 -0
  13. scrapely/events/event_manager.py +292 -0
  14. scrapely/log.py +39 -0
  15. scrapely/patch_browser_pool.py +861 -0
  16. scrapely/pricing_types.py +172 -0
  17. scrapely/proxy_configuration.py +741 -0
  18. scrapely/py.typed +2 -0
  19. scrapely/request_loaders/__init__.py +5 -0
  20. scrapely/request_loaders/_scrapely_request_list.py +169 -0
  21. scrapely/storage/__init__.py +48 -0
  22. scrapely/storage/_file_system/__init__.py +9 -0
  23. scrapely/storage/_file_system/_key_value_store_client.py +155 -0
  24. scrapely/storage/_file_system/_storage_client.py +50 -0
  25. scrapely/storage/_metadata.py +138 -0
  26. scrapely/storage/api_client.py +350 -0
  27. scrapely/storage/client.py +618 -0
  28. scrapely/storage/patch_storage.py +257 -0
  29. scrapely/storage/scrapely_dataset_client.py +305 -0
  30. scrapely/storage/scrapely_kvs_client.py +220 -0
  31. scrapely/storage/scrapely_rq_client.py +496 -0
  32. scrapely/storage/scrapely_storage_client.py +192 -0
  33. scrapely/storage/smart_scrapely_storage_client.py +109 -0
  34. scrapely/storage/types.py +601 -0
  35. scrapely/storage/utils.py +38 -0
  36. scrapely/storage_clients/__init__.py +25 -0
  37. scrapely/storages/__init__.py +5 -0
  38. scrapely/types.py +269 -0
  39. scrapely/webhook.py +82 -0
  40. scrapely_python-0.1.0.dist-info/METADATA +83 -0
  41. scrapely_python-0.1.0.dist-info/RECORD +43 -0
  42. scrapely_python-0.1.0.dist-info/WHEEL +5 -0
  43. scrapely_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,107 @@
1
+ """Charging-aware API client for Scrapely.
2
+
3
+ Creates a ``scrapely_client`` ``ApifyClientAsync`` configured with charging
4
+ hooks, so pushes to the run's default dataset are intercepted and charged
5
+ pay-per-event by the client itself. The charging logic (pricing, budgets,
6
+ ledger) stays in the SDK's ``ChargingManager``; the client only routes
7
+ ``push_items()`` through the hooks.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence
13
+
14
+ from scrapely_client import ApifyClientAsync
15
+
16
+ # Re-exported so the SDK and the client share one marker attribute and one
17
+ # context var. Identity matters: the client's intercepted ``push_items()``
18
+ # reads/writes these, and the SDK detects interception via the attribute.
19
+ from scrapely_client import USES_PUSH_DATA_INTERCEPTION, push_data_charging_context
20
+
21
+ if TYPE_CHECKING:
22
+ from .charging import ChargingManager
23
+
24
+ __all__ = [
25
+ 'USES_PUSH_DATA_INTERCEPTION',
26
+ 'push_data_charging_context',
27
+ 'create_charging_client',
28
+ ]
29
+
30
+
31
+ class _ChargingManagerHooks:
32
+ """Adapts the SDK's ``ChargingManager`` to ``scrapely_client``'s ``ChargingHooks`` protocol."""
33
+
34
+ def __init__(self, charging_manager: 'ChargingManager') -> None:
35
+ self._charging_manager = charging_manager
36
+
37
+ def should_charge_dataset_pushes(self, dataset_id: str) -> bool:
38
+ """Whether default-dataset-item event pricing is configured.
39
+
40
+ Called by the client at ``dataset()`` time, so pricing is consulted
41
+ lazily, at the same moment the former subclass override did.
42
+ """
43
+ from .charging import has_default_dataset_item_event_in_pricing
44
+
45
+ pricing_info = self._charging_manager.get_pricing_info()
46
+ return has_default_dataset_item_event_in_pricing(pricing_info.per_event_prices or {})
47
+
48
+ async def push_and_charge(
49
+ self,
50
+ *,
51
+ items: Sequence[Dict[str, Any]],
52
+ event_name: Optional[str],
53
+ push_fn: Callable[[Sequence[Dict[str, Any]]], Any],
54
+ ) -> Any:
55
+ from .charging import push_data_and_charge
56
+
57
+ # The client only intercepts the default dataset.
58
+ return await push_data_and_charge(
59
+ self._charging_manager,
60
+ list(items),
61
+ event_name,
62
+ True,
63
+ push_fn,
64
+ )
65
+
66
+ def merge_charge_results(self, a: Any, b: Any) -> Any:
67
+ from .charging import merge_charge_results
68
+
69
+ return merge_charge_results(a, b)
70
+
71
+
72
+ def create_charging_client(
73
+ *,
74
+ token: Optional[str] = None,
75
+ api_url: Optional[str] = None,
76
+ api_public_url: Optional[str] = None,
77
+ charging_manager: Optional['ChargingManager'] = None,
78
+ default_dataset_id: Optional[str] = None,
79
+ **kwargs: Any,
80
+ ) -> ApifyClientAsync:
81
+ """Create an ``ApifyClientAsync`` configured with the SDK's charging hooks.
82
+
83
+ Args:
84
+ token: API token
85
+ api_url: Base URL for the API
86
+ api_public_url: Publicly accessible base URL for signed/public resource URLs.
87
+ When omitted, the client's own default is used.
88
+ charging_manager: Optional charging manager for pay-per-event
89
+ default_dataset_id: The default dataset ID for this actor run
90
+ **kwargs: Additional arguments passed to ApifyClientAsync
91
+
92
+ Returns:
93
+ A charging-configured ``ApifyClientAsync``
94
+ """
95
+ if api_url:
96
+ kwargs['api_url'] = api_url
97
+ if api_public_url:
98
+ kwargs['api_public_url'] = api_public_url
99
+
100
+ charging_hooks = _ChargingManagerHooks(charging_manager) if charging_manager is not None else None
101
+
102
+ return ApifyClientAsync(
103
+ token=token,
104
+ charging_hooks=charging_hooks,
105
+ default_dataset_id=default_dataset_id,
106
+ **kwargs,
107
+ )
@@ -0,0 +1,375 @@
1
+ """
2
+ Configuration management for the Scrapely Python SDK.
3
+
4
+ Handles environment variables and configuration options for Actors.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from datetime import datetime
10
+ from typing import Any, Dict, Optional, cast
11
+
12
+ from .types import ActorPermissionLevel, ApifyEnv
13
+
14
+
15
+
16
+ class Configuration:
17
+ """Configuration management for Scrapely Actors.
18
+
19
+ Handles environment variables and provides default values.
20
+ Compatible with Apify's configuration system.
21
+ """
22
+
23
+ # Environment variable to configuration key mapping
24
+ ENV_MAP = {
25
+ # APIFY_* variables
26
+ 'APIFY_TOKEN': 'token',
27
+ 'APIFY_API_BASE_URL': 'api_base_url',
28
+ 'APIFY_API_PUBLIC_BASE_URL': 'api_public_base_url',
29
+ 'APIFY_IS_AT_HOME': 'is_at_home',
30
+ 'APIFY_ACTOR_EVENTS_WS_URL': 'actor_events_ws_url',
31
+ 'APIFY_ACTOR_ID': 'actor_id',
32
+ 'APIFY_ACTOR_RUN_ID': 'actor_run_id',
33
+ 'APIFY_ACTOR_TASK_ID': 'actor_task_id',
34
+ 'APIFY_CONTAINER_PORT': 'container_port',
35
+ 'APIFY_CONTAINER_URL': 'container_url',
36
+ 'APIFY_USER_ID': 'user_id',
37
+ 'APIFY_USER_IS_PAYING': 'user_is_paying',
38
+ 'APIFY_PROXY_HOSTNAME': 'proxy_hostname',
39
+ 'APIFY_PROXY_PASSWORD': 'proxy_password',
40
+ 'APIFY_PROXY_STATUS_URL': 'proxy_status_url',
41
+ 'APIFY_PROXY_PORT': 'proxy_port',
42
+ 'APIFY_INPUT_SECRETS_PRIVATE_KEY_FILE': 'input_secrets_private_key_file',
43
+ 'APIFY_INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE': 'input_secrets_private_key_passphrase',
44
+ 'APIFY_META_ORIGIN': 'meta_origin',
45
+ 'APIFY_DEFAULT_DATASET_ID': 'default_dataset_id',
46
+ 'APIFY_DEFAULT_KEY_VALUE_STORE_ID': 'default_key_value_store_id',
47
+ 'APIFY_DEFAULT_REQUEST_QUEUE_ID': 'default_request_queue_id',
48
+ 'APIFY_MEMORY_MBYTES': 'memory_mbytes',
49
+ 'APIFY_INPUT_KEY': 'input_key',
50
+ 'APIFY_ACTOR_PRICING_INFO': 'actor_pricing_info',
51
+ 'APIFY_CHARGED_ACTOR_EVENT_COUNTS': 'charged_event_counts',
52
+ 'APIFY_USER_PRICING_TIER': 'user_pricing_tier',
53
+ 'APIFY_LOG_LEVEL': 'log_level',
54
+ 'APIFY_LOG_FORMAT': 'log_format',
55
+ 'APIFY_LOCAL_STORAGE_DIR': 'local_storage_dir',
56
+ 'APIFY_HEADLESS': 'headless',
57
+ 'APIFY_SDK_LATEST_VERSION': 'sdk_latest_version',
58
+ 'APIFY_SYSTEM_INFO_INTERVAL_MILLIS': 'system_info_interval_millis',
59
+ 'APIFY_WORKFLOW_KEY': 'workflow_key',
60
+ 'APIFY_DEDICATED_CPUS': 'dedicated_cpus',
61
+ 'APIFY_DISABLE_OUTDATED_WARNING': 'disable_outdated_warning',
62
+ 'APIFY_FACT': 'fact',
63
+ 'APIFY_CHROME_EXECUTABLE_PATH': 'chrome_executable_path',
64
+ 'APIFY_PURGE_ON_START': 'purge_on_start',
65
+ 'APIFY_XVFB': 'xvfb',
66
+ 'APIFY_METAMORPH_AFTER_SLEEP_MILLIS': 'metamorph_after_sleep_millis',
67
+ 'APIFY_PERSIST_STATE_INTERVAL_MILLIS': 'persist_state_interval_millis',
68
+ 'APIFY_ACTOR_BUILD_ID': 'actor_build_id',
69
+ 'APIFY_ACTOR_BUILD_NUMBER': 'actor_build_number',
70
+ 'APIFY_ACTOR_MAX_PAID_DATASET_ITEMS': 'actor_max_paid_dataset_items',
71
+ 'APIFY_STARTED_AT': 'started_at',
72
+ 'APIFY_TIMEOUT_AT': 'timeout_at',
73
+
74
+ # SCRAPELY_* variables
75
+
76
+ 'SCRAPELY_TOKEN': 'token',
77
+ 'SCRAPELY_API_URL': 'api_base_url',
78
+ 'SCRAPELY_IS_AT_HOME': 'is_at_home',
79
+
80
+ # ACTOR_* variables
81
+ 'ACTOR_ID': 'actor_id',
82
+ 'ACTOR_RUN_ID': 'actor_run_id',
83
+ 'ACTOR_TASK_ID': 'actor_task_id',
84
+ 'ACTOR_DEFAULT_DATASET_ID': 'default_dataset_id',
85
+ 'ACTOR_DEFAULT_KEY_VALUE_STORE_ID': 'default_key_value_store_id',
86
+ 'ACTOR_DEFAULT_REQUEST_QUEUE_ID': 'default_request_queue_id',
87
+ 'ACTOR_EVENTS_WEBSOCKET_URL': 'actor_events_ws_url',
88
+ 'ACTOR_INPUT_KEY': 'input_key',
89
+ 'ACTOR_MEMORY_MBYTES': 'memory_mbytes',
90
+ 'ACTOR_PERMISSION_LEVEL': 'actor_permission_level',
91
+ 'ACTOR_WEB_SERVER_PORT': 'container_port',
92
+ 'ACTOR_WEB_SERVER_URL': 'container_url',
93
+ 'ACTOR_MAX_TOTAL_CHARGE_USD': 'max_total_charge_usd',
94
+ 'ACTOR_TEST_PAY_PER_EVENT': 'test_pay_per_event',
95
+ 'ACTOR_USE_CHARGING_LOG_DATASET': 'use_charging_log_dataset',
96
+ 'ACTOR_STORAGES_JSON': 'actor_storages_json',
97
+ 'ACTOR_META_ORIGIN': 'meta_origin',
98
+ 'ACTOR_TIMEOUT_AT': 'timeout_at',
99
+ 'ACTOR_STARTED_AT': 'started_at',
100
+ 'ACTOR_BUILD_ID': 'actor_build_id',
101
+ 'ACTOR_BUILD_NUMBER': 'actor_build_number',
102
+ 'ACTOR_BUILD_TAGS': 'actor_build_tags',
103
+ 'ACTOR_FULL_NAME': 'actor_full_name',
104
+ 'ACTOR_INPUT': 'actor_input',
105
+ 'ACTOR_METAMORPH_COUNT': 'metamorph_count',
106
+ 'ACTOR_MAX_PAID_DATASET_ITEMS': 'actor_max_paid_dataset_items',
107
+ 'ACTOR_RESTART_ON_ERROR': 'restart_on_error',
108
+ 'ACTOR_STANDBY_PORT': 'standby_port',
109
+ 'ACTOR_STANDBY_URL': 'standby_url',
110
+
111
+ # CRAWLEE_* variables
112
+ 'CRAWLEE_STORAGE_DIR': 'local_storage_dir',
113
+ }
114
+
115
+
116
+ # Variables that should be parsed as integers
117
+ INTEGER_VARS = {
118
+ 'container_port', 'proxy_port', 'memory_mbytes',
119
+ 'persist_state_interval_millis',
120
+ 'metamorph_after_sleep_millis', 'actor_max_paid_dataset_items',
121
+ 'standby_port', 'dedicated_cpus', 'disable_outdated_warning',
122
+ }
123
+
124
+ # Variables that should be parsed as floats (USD amounts etc.)
125
+ FLOAT_VARS = {
126
+ 'max_total_charge_usd',
127
+ }
128
+
129
+ # Variables that should be parsed as booleans
130
+ BOOLEAN_VARS = {
131
+ 'is_at_home', 'test_pay_per_event', 'use_charging_log_dataset',
132
+ 'user_is_paying', 'purge_on_start', 'headless', 'restart_on_error',
133
+ }
134
+
135
+
136
+ # Default values
137
+ DEFAULTS = {
138
+ 'default_key_value_store_id': 'default',
139
+ 'default_dataset_id': 'default',
140
+ 'default_request_queue_id': 'default',
141
+ 'input_key': 'INPUT',
142
+ 'api_base_url': 'https://api-staging.scrape.ly',
143
+ 'api_public_base_url': 'https://api-staging.scrape.ly',
144
+ 'proxy_status_url': 'http://proxy.scrape.ly',
145
+ 'proxy_hostname': 'proxy.scrape.ly',
146
+ 'proxy_port': 8000,
147
+ 'container_port': 4321,
148
+ 'container_url': 'http://localhost:4321',
149
+ 'metamorph_after_sleep_millis': 300000,
150
+ 'persist_state_interval_millis': 60000,
151
+ 'test_pay_per_event': False,
152
+ 'use_charging_log_dataset': False,
153
+ }
154
+
155
+ _instance: Optional['Configuration'] = None
156
+
157
+ def __init__(self, options: Optional[Dict[str, Any]] = None):
158
+ """Initialize configuration with optional overrides.
159
+
160
+ Args:
161
+ options: Configuration options to override defaults
162
+ """
163
+ self._config: Dict[str, Any] = {}
164
+ self._options = options or {}
165
+
166
+ # Load from environment and options
167
+ self._load_from_env()
168
+ self._load_from_options()
169
+
170
+ @classmethod
171
+ def get_global_config(cls) -> 'Configuration':
172
+ """Get or create the global configuration instance."""
173
+ if cls._instance is None:
174
+ cls._instance = cls()
175
+ return cls._instance
176
+
177
+ @classmethod
178
+ def get_global_configuration(cls) -> 'Configuration':
179
+ """Apify/crawlee-compatible alias for get_global_config()."""
180
+ return cls.get_global_config()
181
+
182
+ @classmethod
183
+ def reset_global_state(cls) -> None:
184
+ """Reset the global configuration instance."""
185
+ cls._instance = None
186
+
187
+ def _load_from_env(self) -> None:
188
+ """Load configuration from environment variables."""
189
+ for env_key, config_key in self.ENV_MAP.items():
190
+ value = os.environ.get(env_key)
191
+ if value is not None:
192
+ self._config[config_key] = self._parse_value(config_key, value)
193
+
194
+ def _load_from_options(self) -> None:
195
+ """Load configuration from provided options."""
196
+ for key, value in self._options.items():
197
+ if value is not None:
198
+ self._config[key] = value
199
+
200
+ def _parse_value(self, key: str, value: str) -> Any:
201
+ """Parse a string value based on the key type."""
202
+ if key in self.INTEGER_VARS:
203
+ try:
204
+ return int(value)
205
+ except ValueError:
206
+ return value
207
+ elif key in self.FLOAT_VARS:
208
+ try:
209
+ return float(value)
210
+ except ValueError:
211
+ return value
212
+ elif key in self.BOOLEAN_VARS:
213
+ return value.lower() in ('1', 'true', 'yes')
214
+ elif key in ('timeout_at', 'started_at'):
215
+ try:
216
+ return datetime.fromisoformat(value.replace('Z', '+00:00'))
217
+ except (ValueError, TypeError):
218
+ return value
219
+ return value
220
+
221
+ def get(self, key: str, default: Any = None) -> Any:
222
+ """Get a configuration value.
223
+
224
+ Args:
225
+ key: Configuration key
226
+ default: Default value if not found
227
+
228
+ Returns:
229
+ Configuration value or default
230
+ """
231
+ # Check config first (from env/options)
232
+ if key in self._config:
233
+ return self._config[key]
234
+ # Check defaults
235
+ if key in self.DEFAULTS:
236
+ return self.DEFAULTS[key]
237
+ return default
238
+
239
+ def set(self, key: str, value: Any) -> None:
240
+ """Set a configuration value.
241
+
242
+ Args:
243
+ key: Configuration key
244
+ value: Value to set
245
+ """
246
+ self._config[key] = value
247
+
248
+ @property
249
+ def token(self) -> Optional[str]:
250
+ """Get the authentication token."""
251
+ return cast(Optional[str], self.get('token'))
252
+
253
+ @property
254
+ def api_base_url(self) -> str:
255
+ """Get the API base URL."""
256
+ return cast(str, self.get('api_base_url', self.DEFAULTS['api_base_url']))
257
+
258
+ @property
259
+ def actor_run_id(self) -> Optional[str]:
260
+ """Get the current Actor run ID."""
261
+ return cast(Optional[str], self.get('actor_run_id'))
262
+
263
+ @property
264
+ def actor_id(self) -> Optional[str]:
265
+ """Get the current Actor ID."""
266
+ return cast(Optional[str], self.get('actor_id'))
267
+
268
+ @property
269
+ def is_at_home(self) -> bool:
270
+ """Check if running on the Scrapely platform."""
271
+ return cast(bool, self.get('is_at_home', False))
272
+
273
+ @property
274
+ def default_dataset_id(self) -> str:
275
+ """Get the default dataset ID."""
276
+ return cast(str, self.get('default_dataset_id', self.DEFAULTS['default_dataset_id']))
277
+
278
+ @property
279
+ def default_key_value_store_id(self) -> str:
280
+ """Get the default key-value store ID."""
281
+ return cast(str, self.get('default_key_value_store_id', self.DEFAULTS['default_key_value_store_id']))
282
+
283
+ @property
284
+ def default_request_queue_id(self) -> str:
285
+ """Get the default request queue ID."""
286
+ return cast(str, self.get('default_request_queue_id', self.DEFAULTS['default_request_queue_id']))
287
+
288
+ @property
289
+ def input_key(self) -> str:
290
+ """Get the input key."""
291
+ return cast(str, self.get('input_key', self.DEFAULTS['input_key']))
292
+
293
+ def get_env(self) -> ApifyEnv:
294
+ """Get all environment variables as an ApifyEnv object.
295
+
296
+ Returns:
297
+ ApifyEnv object with all parsed environment variables
298
+ """
299
+ return ApifyEnv(
300
+ actor_id=self.get('actor_id'),
301
+ actor_build_id=self.get('actor_build_id'),
302
+ actor_build_number=self.get('actor_build_number'),
303
+ actor_build_tags=self.get('actor_build_tags'),
304
+ actor_events_ws_url=self.get('actor_events_ws_url'),
305
+ actor_full_name=self.get('actor_full_name'),
306
+ actor_max_paid_dataset_items=self.get('actor_max_paid_dataset_items'),
307
+ actor_permission_level=self._parse_permission_level(self.get('actor_permission_level')),
308
+ actor_pricing_info=(
309
+ json.dumps(self.get('actor_pricing_info'))
310
+ if isinstance(self.get('actor_pricing_info'), dict)
311
+ else self.get('actor_pricing_info')
312
+ ),
313
+ actor_run_id=self.get('actor_run_id'),
314
+ actor_storages_json=self.get('actor_storages_json'),
315
+ actor_task_id=self.get('actor_task_id'),
316
+ api_base_url=self.get('api_base_url'),
317
+ api_public_base_url=self.get('api_public_base_url'),
318
+ charged_event_counts=(
319
+ json.dumps(self.get('charged_event_counts'))
320
+ if isinstance(self.get('charged_event_counts'), dict)
321
+ else self.get('charged_event_counts')
322
+ ),
323
+
324
+ chrome_executable_path=self.get('chrome_executable_path'),
325
+ container_port=self.get('container_port'),
326
+ container_url=self.get('container_url'),
327
+ dedicated_cpus=str(self.get('dedicated_cpus')) if self.get('dedicated_cpus') is not None else None,
328
+ default_dataset_id=self.get('default_dataset_id'),
329
+ default_key_value_store_id=self.get('default_key_value_store_id'),
330
+ default_request_queue_id=self.get('default_request_queue_id'),
331
+ disable_outdated_warning=self.get('disable_outdated_warning'),
332
+ fact=self.get('fact'),
333
+ headless='1' if self.get('headless') else None,
334
+ input_key=self.get('input_key'),
335
+ input_secrets_private_key_file=self.get('input_secrets_private_key_file'),
336
+ input_secrets_private_key_passphrase=self.get('input_secrets_private_key_passphrase'),
337
+ is_at_home='1' if self.get('is_at_home') else None,
338
+ local_storage_dir=self.get('local_storage_dir'),
339
+ log_format=self.get('log_format'),
340
+ log_level=self.get('log_level'),
341
+ memory_mbytes=self.get('memory_mbytes'),
342
+ meta_origin=self.get('meta_origin'),
343
+ metamorph_after_sleep_millis=self.get('metamorph_after_sleep_millis'),
344
+ persist_state_interval_millis=self.get('persist_state_interval_millis'),
345
+ proxy_hostname=self.get('proxy_hostname'),
346
+ proxy_password=self.get('proxy_password'),
347
+ proxy_port=str(self.get('proxy_port')) if self.get('proxy_port') is not None else None,
348
+ proxy_status_url=self.get('proxy_status_url'),
349
+ purge_on_start='1' if self.get('purge_on_start') else None,
350
+ sdk_latest_version=self.get('sdk_latest_version'),
351
+ started_at=self.get('started_at'),
352
+ standby_port=self.get('standby_port'),
353
+ standby_url=self.get('standby_url'),
354
+ system_info_interval_millis=self.get('system_info_interval_millis'),
355
+ timeout_at=self.get('timeout_at'),
356
+ token=self.get('token'),
357
+ user_id=self.get('user_id'),
358
+ user_is_paying='1' if self.get('user_is_paying') else None,
359
+ user_pricing_tier=self.get('user_pricing_tier'),
360
+ workflow_key=self.get('workflow_key'),
361
+ xvfb=self.get('xvfb'),
362
+ max_total_charge_usd=self.get('max_total_charge_usd'),
363
+ user_plan=self.get('user_plan'),
364
+ restart_on_error=self.get('restart_on_error'),
365
+ )
366
+
367
+
368
+ def _parse_permission_level(self, value: Optional[str]) -> Optional[ActorPermissionLevel]:
369
+ """Parse permission level string to enum."""
370
+ if value is None:
371
+ return None
372
+ try:
373
+ return ActorPermissionLevel(value.upper())
374
+ except ValueError:
375
+ return None
@@ -0,0 +1,21 @@
1
+ """Crypto helpers for the Scrapely Python SDK.
2
+
3
+ Currently contains the secret Actor input decryption ported from the Node SDK's
4
+ ``src/crypto/secrets.ts``.
5
+ """
6
+
7
+ from ._secrets import (
8
+ decrypt_all_secrets,
9
+ decrypt_private_key_file,
10
+ decrypt_secret_inputs,
11
+ decrypt_secret_value,
12
+ merge_decrypted_input,
13
+ )
14
+
15
+ __all__ = [
16
+ 'decrypt_all_secrets',
17
+ 'decrypt_private_key_file',
18
+ 'decrypt_secret_inputs',
19
+ 'decrypt_secret_value',
20
+ 'merge_decrypted_input',
21
+ ]