scrapely-client-python 1.0.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.

Potentially problematic release.


This version of scrapely-client-python might be problematic. Click here for more details.

Files changed (65) hide show
  1. scrapely_client/__init__.py +30 -0
  2. scrapely_client/_charging.py +186 -0
  3. scrapely_client/_client_registry.py +136 -0
  4. scrapely_client/_consts.py +96 -0
  5. scrapely_client/_docs.py +37 -0
  6. scrapely_client/_literals.py +539 -0
  7. scrapely_client/_logging.py +180 -0
  8. scrapely_client/_models.py +4172 -0
  9. scrapely_client/_pagination.py +220 -0
  10. scrapely_client/_resource_clients/__init__.py +84 -0
  11. scrapely_client/_resource_clients/_resource_client.py +567 -0
  12. scrapely_client/_resource_clients/actor.py +1042 -0
  13. scrapely_client/_resource_clients/actor_collection.py +377 -0
  14. scrapely_client/_resource_clients/actor_env_var.py +154 -0
  15. scrapely_client/_resource_clients/actor_env_var_collection.py +165 -0
  16. scrapely_client/_resource_clients/actor_version.py +254 -0
  17. scrapely_client/_resource_clients/actor_version_collection.py +232 -0
  18. scrapely_client/_resource_clients/build.py +244 -0
  19. scrapely_client/_resource_clients/build_collection.py +164 -0
  20. scrapely_client/_resource_clients/dataset.py +1151 -0
  21. scrapely_client/_resource_clients/dataset_collection.py +244 -0
  22. scrapely_client/_resource_clients/key_value_store.py +906 -0
  23. scrapely_client/_resource_clients/key_value_store_collection.py +243 -0
  24. scrapely_client/_resource_clients/log.py +225 -0
  25. scrapely_client/_resource_clients/request_queue.py +1167 -0
  26. scrapely_client/_resource_clients/request_queue_collection.py +240 -0
  27. scrapely_client/_resource_clients/run.py +875 -0
  28. scrapely_client/_resource_clients/run_collection.py +226 -0
  29. scrapely_client/_resource_clients/schedule.py +243 -0
  30. scrapely_client/_resource_clients/schedule_collection.py +260 -0
  31. scrapely_client/_resource_clients/store_collection.py +242 -0
  32. scrapely_client/_resource_clients/task.py +793 -0
  33. scrapely_client/_resource_clients/task_collection.py +322 -0
  34. scrapely_client/_resource_clients/user.py +262 -0
  35. scrapely_client/_resource_clients/webhook.py +292 -0
  36. scrapely_client/_resource_clients/webhook_collection.py +281 -0
  37. scrapely_client/_resource_clients/webhook_dispatch.py +84 -0
  38. scrapely_client/_resource_clients/webhook_dispatch_collection.py +158 -0
  39. scrapely_client/_scrapely_client.py +837 -0
  40. scrapely_client/_statistics.py +29 -0
  41. scrapely_client/_status_message_watcher.py +197 -0
  42. scrapely_client/_streamed_log.py +250 -0
  43. scrapely_client/_typeddicts.py +408 -0
  44. scrapely_client/_utils/__init__.py +0 -0
  45. scrapely_client/_utils/crypto.py +87 -0
  46. scrapely_client/_utils/encoding.py +133 -0
  47. scrapely_client/_utils/errors.py +33 -0
  48. scrapely_client/_utils/http.py +137 -0
  49. scrapely_client/_utils/time.py +32 -0
  50. scrapely_client/_utils/try_import.py +61 -0
  51. scrapely_client/errors.py +190 -0
  52. scrapely_client/http_clients/__init__.py +10 -0
  53. scrapely_client/http_clients/_base.py +430 -0
  54. scrapely_client/http_clients/_impit.py +598 -0
  55. scrapely_client/http_compressors/__init__.py +18 -0
  56. scrapely_client/http_compressors/_base.py +25 -0
  57. scrapely_client/http_compressors/_brotli.py +38 -0
  58. scrapely_client/http_compressors/_gzip.py +38 -0
  59. scrapely_client/http_compressors/_resolve.py +38 -0
  60. scrapely_client/py.typed +0 -0
  61. scrapely_client/types.py +52 -0
  62. scrapely_client_python-1.0.0.dist-info/METADATA +244 -0
  63. scrapely_client_python-1.0.0.dist-info/RECORD +65 -0
  64. scrapely_client_python-1.0.0.dist-info/WHEEL +4 -0
  65. scrapely_client_python-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import inspect
5
+ import logging
6
+ import threading
7
+ from contextvars import ContextVar
8
+ from typing import TYPE_CHECKING, Any, NamedTuple
9
+
10
+ from colorama import Fore, Style
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ from scrapely_client._resource_clients._resource_client import ResourceClientBase
16
+
17
+ _BaseClient = ResourceClientBase
18
+
19
+
20
+ logger_name = __name__.split('.')[0]
21
+ """Name of the logger used throughout the library."""
22
+
23
+ logger = logging.getLogger(logger_name)
24
+ """Logger used throughout the library."""
25
+
26
+
27
+ class LogContext(NamedTuple):
28
+ """Request context details for logging (attempt, client method, HTTP method, resource ID, URL)."""
29
+
30
+ attempt: ContextVar[int | None]
31
+ client_method: ContextVar[str | None]
32
+ method: ContextVar[str | None]
33
+ resource_id: ContextVar[str | None]
34
+ url: ContextVar[str | None]
35
+
36
+
37
+ log_context = LogContext(
38
+ attempt=ContextVar('attempt', default=None),
39
+ client_method=ContextVar('client_method', default=None),
40
+ method=ContextVar('method', default=None),
41
+ resource_id=ContextVar('resource_id', default=None),
42
+ url=ContextVar('url', default=None),
43
+ )
44
+
45
+
46
+ class LoggerOnce:
47
+ """Emits each log message at most once, keyed by an explicit string.
48
+
49
+ Useful for diagnostic warnings that would otherwise spam the log when the same condition recurs (per-request
50
+ misconfiguration warnings, repeated fallback paths, etc.). Deduplication scope follows the lifetime of the
51
+ instance - a module-level instance gives process-wide dedup; an attribute on a class gives per-instance dedup.
52
+
53
+ Safe to call from multiple threads, since the sync client can be used from several threads at once.
54
+ """
55
+
56
+ def __init__(self, logger: logging.Logger) -> None:
57
+ self._logger = logger
58
+ self._seen: set[str] = set()
59
+ self._lock = threading.Lock()
60
+
61
+ def log(self, message: str, *, key: str, level: int = logging.INFO) -> None:
62
+ """Log `message` at `level` the first time `key` is seen on this instance; later calls are no-ops.
63
+
64
+ Args:
65
+ message: The message to log.
66
+ key: Deduplication key. Two calls with the same key emit at most once.
67
+ level: Standard `logging` level (e.g. `logging.WARNING`). Defaults to `logging.INFO`.
68
+ """
69
+ # The check and the insert have to be atomic, otherwise two threads racing on the same key both emit.
70
+ with self._lock:
71
+ if key in self._seen:
72
+ return
73
+ self._seen.add(key)
74
+
75
+ self._logger.log(level, message)
76
+
77
+
78
+ class WithLogDetailsClient(type):
79
+ """Metaclass that wraps public methods to inject client details into log context."""
80
+
81
+ def __new__(cls, name: str, bases: tuple, attrs: dict) -> WithLogDetailsClient:
82
+ """Wrap all public methods in the class with logging context injection."""
83
+ for attr_name, attr_value in attrs.items():
84
+ if not attr_name.startswith('_') and inspect.isfunction(attr_value):
85
+ attrs[attr_name] = _injects_client_details_to_log_context(attr_value)
86
+
87
+ return type.__new__(cls, name, bases, attrs)
88
+
89
+
90
+ class RedirectLogFormatter(logging.Formatter):
91
+ """Log formatter that prepends colored logger name to messages."""
92
+
93
+ def format(self, record: logging.LogRecord) -> str:
94
+ """Format log by prepending colored logger name.
95
+
96
+ Args:
97
+ record: The log record to format.
98
+
99
+ Returns:
100
+ Formatted log message with colored logger name prefix.
101
+ """
102
+ formatted_logger_name = f'{Fore.CYAN}[{record.name}]{Style.RESET_ALL}'
103
+ formatted_message = super().format(record)
104
+ return f'{formatted_logger_name} -> {formatted_message}'
105
+
106
+
107
+ def create_redirect_logger(name: str) -> logging.Logger:
108
+ """Create a logger for redirecting logs from another Actor.
109
+
110
+ Args:
111
+ name: Logger name. Use dot notation for hierarchy (e.g., "apify.xyz" creates "xyz" under "apify").
112
+
113
+ Returns:
114
+ Configured logger with RedirectLogFormatter.
115
+ """
116
+ to_logger = logging.getLogger(name)
117
+ to_logger.propagate = False
118
+
119
+ # Remove filters and handlers in case this logger already exists and was set up in some way.
120
+ for handler in list(to_logger.handlers):
121
+ to_logger.removeHandler(handler)
122
+ for log_filter in list(to_logger.filters):
123
+ to_logger.removeFilter(log_filter)
124
+
125
+ handler = logging.StreamHandler()
126
+ handler.setFormatter(RedirectLogFormatter())
127
+ to_logger.addHandler(handler)
128
+ to_logger.setLevel(logging.DEBUG)
129
+ return to_logger
130
+
131
+
132
+ class _ContextInjectingFilter(logging.Filter):
133
+ """Filter that injects current log context into all log records."""
134
+
135
+ def filter(self, record: logging.LogRecord) -> bool:
136
+ """Add log context variables to the record."""
137
+ record.client_method = log_context.client_method.get()
138
+ record.resource_id = log_context.resource_id.get()
139
+ record.method = log_context.method.get()
140
+ record.url = log_context.url.get()
141
+ record.attempt = log_context.attempt.get()
142
+ return True
143
+
144
+
145
+ def _injects_client_details_to_log_context(fun: Callable) -> Callable:
146
+ """Wrap a method to inject resource client details into log context before execution."""
147
+ if inspect.iscoroutinefunction(fun):
148
+
149
+ @functools.wraps(fun)
150
+ async def async_wrapper(resource_client: _BaseClient, *args: Any, **kwargs: Any) -> Any:
151
+ log_context.client_method.set(fun.__qualname__) # ty: ignore[unresolved-attribute]
152
+ log_context.resource_id.set(resource_client._resource_id) # noqa: SLF001
153
+
154
+ return await fun(resource_client, *args, **kwargs)
155
+
156
+ return async_wrapper
157
+
158
+ if inspect.isasyncgenfunction(fun):
159
+
160
+ @functools.wraps(fun)
161
+ async def async_generator_wrapper(resource_client: _BaseClient, *args: Any, **kwargs: Any) -> Any:
162
+ log_context.client_method.set(fun.__qualname__) # ty: ignore[unresolved-attribute]
163
+ log_context.resource_id.set(resource_client._resource_id) # noqa: SLF001
164
+
165
+ async for item in fun(resource_client, *args, **kwargs):
166
+ yield item
167
+
168
+ return async_generator_wrapper
169
+
170
+ @functools.wraps(fun)
171
+ def wrapper(resource_client: _BaseClient, *args: Any, **kwargs: Any) -> Any:
172
+ log_context.client_method.set(fun.__qualname__) # ty: ignore[unresolved-attribute]
173
+ log_context.resource_id.set(resource_client._resource_id) # noqa: SLF001
174
+
175
+ return fun(resource_client, *args, **kwargs)
176
+
177
+ return wrapper
178
+
179
+
180
+ logger.addFilter(_ContextInjectingFilter())