apify-client 3.2.1b2__tar.gz → 3.2.1b4__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 (68) hide show
  1. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/CHANGELOG.md +2 -0
  2. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/PKG-INFO +1 -1
  3. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/pyproject.toml +1 -1
  4. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/pyproject.toml.orig +1 -1
  5. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_status_message_watcher.py +30 -13
  6. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_streamed_log.py +54 -9
  7. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/types.py +2 -1
  8. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/CONTRIBUTING.md +0 -0
  9. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/LICENSE +0 -0
  10. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/README.md +0 -0
  11. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/__init__.py +0 -0
  12. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_apify_client.py +0 -0
  13. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_client_registry.py +0 -0
  14. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_consts.py +0 -0
  15. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_docs.py +0 -0
  16. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_literals.py +0 -0
  17. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_logging.py +0 -0
  18. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_models.py +0 -0
  19. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_pagination.py +0 -0
  20. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/__init__.py +0 -0
  21. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/_resource_client.py +0 -0
  22. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/actor.py +0 -0
  23. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/actor_collection.py +0 -0
  24. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/actor_env_var.py +0 -0
  25. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/actor_env_var_collection.py +0 -0
  26. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/actor_version.py +0 -0
  27. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/actor_version_collection.py +0 -0
  28. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/build.py +0 -0
  29. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/build_collection.py +0 -0
  30. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/dataset.py +0 -0
  31. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/dataset_collection.py +0 -0
  32. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/key_value_store.py +0 -0
  33. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/key_value_store_collection.py +0 -0
  34. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/log.py +0 -0
  35. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/request_queue.py +0 -0
  36. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/request_queue_collection.py +0 -0
  37. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/run.py +0 -0
  38. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/run_collection.py +0 -0
  39. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/schedule.py +0 -0
  40. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/schedule_collection.py +0 -0
  41. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/store_collection.py +0 -0
  42. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/task.py +0 -0
  43. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/task_collection.py +0 -0
  44. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/user.py +0 -0
  45. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/webhook.py +0 -0
  46. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/webhook_collection.py +0 -0
  47. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/webhook_dispatch.py +0 -0
  48. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_resource_clients/webhook_dispatch_collection.py +0 -0
  49. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_statistics.py +0 -0
  50. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_typeddicts.py +0 -0
  51. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/__init__.py +0 -0
  52. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/crypto.py +0 -0
  53. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/encoding.py +0 -0
  54. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/errors.py +0 -0
  55. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/http.py +0 -0
  56. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/time.py +0 -0
  57. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/_utils/try_import.py +0 -0
  58. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/errors.py +0 -0
  59. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_clients/__init__.py +0 -0
  60. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_clients/_base.py +0 -0
  61. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_clients/_httpx2.py +0 -0
  62. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_clients/_impit.py +0 -0
  63. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_compressors/__init__.py +0 -0
  64. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_compressors/_base.py +0 -0
  65. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_compressors/_brotli.py +0 -0
  66. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_compressors/_gzip.py +0 -0
  67. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/http_compressors/_resolve.py +0 -0
  68. {apify_client-3.2.1b2 → apify_client-3.2.1b4}/src/apify_client/py.typed +0 -0
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
8
8
  ### 🐛 Bug Fixes
9
9
 
10
10
  - Stop dataset iterators from skipping items when unwind is used ([#1059](https://github.com/apify/apify-client-python/pull/1059)) ([8e30197](https://github.com/apify/apify-client-python/commit/8e301977ecb80b7c01251cacf071a3f6201469ba)) by [@vdusek](https://github.com/vdusek), closes [#1058](https://github.com/apify/apify-client-python/issues/1058)
11
+ - Keep a failing background watcher from breaking a successful actor call ([#1027](https://github.com/apify/apify-client-python/pull/1027)) ([efc009d](https://github.com/apify/apify-client-python/commit/efc009d78f08d3a03a10db386201e48e45d01c2d)) by [@vdusek](https://github.com/vdusek)
12
+ - Make `JsonSerializable` type alias read-only ([#1072](https://github.com/apify/apify-client-python/pull/1072)) ([bd82a26](https://github.com/apify/apify-client-python/commit/bd82a2645d3c64f0e4a30de3a016c16d92d48340)) by [@Pijukatel](https://github.com/Pijukatel)
11
13
 
12
14
 
13
15
  <!-- git-cliff-unreleased-end -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apify_client
3
- Version: 3.2.1b2
3
+ Version: 3.2.1b4
4
4
  Summary: Apify API client for Python
5
5
  Keywords: apify,api,client,automation,crawling,scraping
6
6
  Author: Apify Technologies s.r.o.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_client"
7
- version = "3.2.1b2"
7
+ version = "3.2.1b4"
8
8
  description = "Apify API client for Python"
9
9
  license = "Apache-2.0"
10
10
  license-files = ["LICENSE"]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_client"
7
- version = "3.2.1b2"
7
+ version = "3.2.1b4"
8
8
  description = "Apify API client for Python"
9
9
  authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }]
10
10
  license = "Apache-2.0"
@@ -120,11 +120,19 @@ class StatusMessageWatcherAsync(StatusMessageWatcherBase):
120
120
  await self.stop()
121
121
 
122
122
  async def _log_changed_status_message(self) -> None:
123
- while True:
124
- run_data = await self._run_client.get()
125
- if not self._log_run_data(run_data):
126
- break
127
- await asyncio.sleep(self._check_period)
123
+ try:
124
+ while True:
125
+ run_data = await self._run_client.get()
126
+ if not self._log_run_data(run_data):
127
+ break
128
+ await asyncio.sleep(self._check_period)
129
+ except Exception as exc:
130
+ if self._run_client._http_client.is_timeout_error(exc): # noqa: SLF001
131
+ # An expected timeout, so warn rather than leak a traceback.
132
+ self._to_logger.warning('Status message redirection stopped: the status request timed out.')
133
+ else:
134
+ # A failed poll must not escape into `stop` and surface as a failure of the run.
135
+ self._to_logger.exception('Status message redirection stopped due to unexpected error:')
128
136
 
129
137
 
130
138
  @docs_group('Other')
@@ -159,10 +167,11 @@ class StatusMessageWatcher(StatusMessageWatcherBase):
159
167
 
160
168
  The caller is responsible for cleanup by calling the `stop` method when done.
161
169
  """
162
- if self._logging_thread:
170
+ if self._logging_thread and self._logging_thread.is_alive():
163
171
  raise RuntimeError('Logging thread already active')
164
172
  self._stop_logging = False
165
- self._logging_thread = threading.Thread(target=self._log_changed_status_message)
173
+ # A daemon thread, so a watcher still polling cannot hold up interpreter shutdown.
174
+ self._logging_thread = threading.Thread(target=self._log_changed_status_message, daemon=True)
166
175
  self._logging_thread.start()
167
176
  return self._logging_thread
168
177
 
@@ -189,9 +198,17 @@ class StatusMessageWatcher(StatusMessageWatcherBase):
189
198
  self.stop()
190
199
 
191
200
  def _log_changed_status_message(self) -> None:
192
- while True:
193
- if not self._log_run_data(self._run_client.get()):
194
- break
195
- if self._stop_logging:
196
- break
197
- time.sleep(self._check_period)
201
+ try:
202
+ while True:
203
+ if not self._log_run_data(self._run_client.get()):
204
+ break
205
+ if self._stop_logging:
206
+ break
207
+ time.sleep(self._check_period)
208
+ except Exception as exc:
209
+ if self._run_client._http_client.is_timeout_error(exc): # noqa: SLF001
210
+ # An expected timeout, so warn rather than leak a traceback.
211
+ self._to_logger.warning('Status message redirection stopped: the status request timed out.')
212
+ else:
213
+ # A failed poll must not escape the background thread.
214
+ self._to_logger.exception('Status message redirection stopped due to unexpected error:')
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
15
15
  from types import TracebackType
16
16
 
17
17
  from apify_client._resource_clients import LogClient, LogClientAsync
18
+ from apify_client.http_clients import HttpResponse
18
19
  from apify_client.types import Timeout
19
20
 
20
21
 
@@ -98,6 +99,13 @@ class StreamedLog(StreamedLogBase):
98
99
  call `start` and `stop` manually. Obtain an instance via `RunClient.get_streamed_log`.
99
100
  """
100
101
 
102
+ _stop_timeout_s: ClassVar[float] = 5
103
+ """Upper bound on how long `stop` waits for the streaming thread to finish.
104
+
105
+ Closing the response only ends the read on a transport that honours it, which neither Impit nor HTTPX does, so
106
+ without a bound `stop` would wait for the next chunk, which on a quiet run may be hours away.
107
+ """
108
+
101
109
  def __init__(self, log_client: LogClient, *, to_logger: logging.Logger, from_start: bool = True) -> None:
102
110
  """Initialize `StreamedLog`.
103
111
 
@@ -111,6 +119,7 @@ class StreamedLog(StreamedLogBase):
111
119
  super().__init__(to_logger=to_logger, from_start=from_start)
112
120
  self._log_client = log_client
113
121
  self._streaming_thread: Thread | None = None
122
+ self._log_stream: HttpResponse | None = None
114
123
  self._stop_logging = False
115
124
 
116
125
  def start(self) -> Thread:
@@ -118,7 +127,7 @@ class StreamedLog(StreamedLogBase):
118
127
 
119
128
  The caller is responsible for cleanup by calling the `stop` method when done.
120
129
  """
121
- if self._streaming_thread:
130
+ if self._streaming_thread and self._streaming_thread.is_alive():
122
131
  raise RuntimeError('Streaming thread already active')
123
132
  self._stop_logging = False
124
133
  # A daemon thread so a stream still blocked on a read can never hold up interpreter shutdown.
@@ -127,13 +136,29 @@ class StreamedLog(StreamedLogBase):
127
136
  return self._streaming_thread
128
137
 
129
138
  def stop(self) -> None:
130
- """Signal the streaming thread to stop logging and wait for it to finish."""
139
+ """Signal the streaming thread to stop logging and wait up to `_stop_timeout_s` for it to finish.
140
+
141
+ A thread that outlives the wait is a daemon with `_stop_logging` set, so it exits after at most one more chunk,
142
+ and only then does its buffered tail reach the logger. Its handle is kept while it is alive, so `start` cannot
143
+ revive it beside a second thread on the same buffer.
144
+ """
131
145
  if not self._streaming_thread:
132
146
  raise RuntimeError('Streaming thread is not active')
133
147
  self._stop_logging = True
134
- self._streaming_thread.join()
135
- self._streaming_thread = None
136
- self._stop_logging = False
148
+ # Read once; the streaming thread clears the attribute as soon as the stream ends.
149
+ log_stream = self._log_stream
150
+ if log_stream is not None:
151
+ try:
152
+ log_stream.close()
153
+ except Exception:
154
+ # A failing `close` in a custom transport must not fail the caller.
155
+ self._to_logger.exception('Closing the log stream failed:')
156
+ self._streaming_thread.join(timeout=self._stop_timeout_s)
157
+ if self._streaming_thread.is_alive():
158
+ # Otherwise log messages arriving after `stop` returned have no explanation.
159
+ self._to_logger.debug('Log streaming thread outlived the stop timeout; it ends after the next chunk.')
160
+ else:
161
+ self._streaming_thread = None
137
162
 
138
163
  def __enter__(self) -> Self:
139
164
  """Start the streaming thread within the context. Exiting the context will finish the streaming thread."""
@@ -151,15 +176,30 @@ class StreamedLog(StreamedLogBase):
151
176
  with self._log_client.stream(raw=True, timeout=self._stream_timeout) as log_stream:
152
177
  if not log_stream:
153
178
  return
179
+ # Published so `stop` can close the response.
180
+ self._log_stream = log_stream
154
181
  try:
182
+ # `stop` may have run before the response existed for it to close.
183
+ if self._stop_logging:
184
+ return
155
185
  for data in log_stream.iter_bytes():
156
186
  self._process_new_data(data)
157
187
  if self._stop_logging:
158
188
  break
159
189
  finally:
160
- # Flush the last buffered part even if the read timed out or was stopped.
161
- self._log_buffer_content(include_last_part=True)
190
+ self._log_stream = None
191
+ try:
192
+ # Flush the last buffered part even if the read timed out or was stopped.
193
+ self._log_buffer_content(include_last_part=True)
194
+ except Exception:
195
+ # A truncated stream leaves an undecodable tail, which is worth a traceback even while a stop
196
+ # is in progress.
197
+ self._to_logger.exception('Log redirection stopped due to unexpected error:')
162
198
  except Exception as exc:
199
+ if self._stop_logging:
200
+ # `stop` closed the stream out from under the read, so the failure is expected.
201
+ self._to_logger.debug('Log streaming stopped while `stop` was in progress: %r', exc)
202
+ return
163
203
  if self._log_client._http_client.is_timeout_error(exc): # noqa: SLF001
164
204
  # The stream cannot continue, so warn and let the thread end instead of leaking a traceback.
165
205
  self._to_logger.warning('Log streaming stopped: the log stream request timed out.')
@@ -236,8 +276,13 @@ class StreamedLogAsync(StreamedLogBase):
236
276
  async for data in log_stream.aiter_bytes():
237
277
  self._process_new_data(data)
238
278
  finally:
239
- # Flush the last buffered part even if the task is cancelled by `stop()`.
240
- self._log_buffer_content(include_last_part=True)
279
+ try:
280
+ # Flush the last buffered part even if the task is cancelled by `stop()`.
281
+ self._log_buffer_content(include_last_part=True)
282
+ except Exception:
283
+ # A truncated stream leaves an undecodable tail. Keeping the failure here also keeps the
284
+ # cancellation `stop` raised propagating, so the task ends up cancelled as asyncio expects.
285
+ self._to_logger.exception('Log redirection stopped due to unexpected error:')
241
286
  except Exception as exc:
242
287
  if self._log_client._http_client.is_timeout_error(exc): # noqa: SLF001
243
288
  # A timeout on the long-lived stream is an expected terminal condition, not an error.
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Mapping, Sequence
3
4
  from datetime import timedelta
4
5
  from typing import Literal
5
6
 
@@ -38,7 +39,7 @@ to ad-hoc webhooks (e.g. `condition`) are ignored at runtime. The `*CamelDict` v
38
39
  matching the Apify API spelling.
39
40
  """
40
41
 
41
- JsonSerializable = dict[str, 'JsonSerializable'] | list['JsonSerializable'] | str | int | float | bool | None
42
+ JsonSerializable = Mapping[str, 'JsonSerializable'] | Sequence['JsonSerializable'] | str | int | float | bool | None
42
43
  """Recursive type for JSON-serializable values - primitives plus objects and arrays with JSON-serializable contents.
43
44
 
44
45
  Based on the definition discussed in https://github.com/python/typing/issues/182.
File without changes
File without changes