pyapiary 2.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.
- pyapiary/__init__.py +31 -0
- pyapiary/api_connectors/__init__.py +0 -0
- pyapiary/api_connectors/broker.py +398 -0
- pyapiary/api_connectors/flashpoint.py +195 -0
- pyapiary/api_connectors/generic.py +105 -0
- pyapiary/api_connectors/ipqs.py +68 -0
- pyapiary/api_connectors/spycloud.py +207 -0
- pyapiary/api_connectors/twilio.py +114 -0
- pyapiary/api_connectors/urlscan.py +148 -0
- pyapiary/dbms_connectors/__init__.py +0 -0
- pyapiary/dbms_connectors/elasticsearch.py +143 -0
- pyapiary/dbms_connectors/mongo.py +390 -0
- pyapiary/dbms_connectors/mongo_async.py +323 -0
- pyapiary/dbms_connectors/odbc.py +110 -0
- pyapiary/dbms_connectors/splunk.py +131 -0
- pyapiary/helpers.py +102 -0
- pyapiary/tests/__init__.py +0 -0
- pyapiary/tests/conftest.py +47 -0
- pyapiary/tests/test_broker/test_integration_broker.py +14 -0
- pyapiary/tests/test_broker/test_unit_asyncbroker.py +17 -0
- pyapiary/tests/test_broker/test_unit_broker.py +67 -0
- pyapiary/tests/test_elasticsearch/test_unit_elasticsearch.py +67 -0
- pyapiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml +66 -0
- pyapiary/tests/test_flashpoint/test_integration_flashpoint.py +11 -0
- pyapiary/tests/test_flashpoint/test_unit_async_flashpoint.py +49 -0
- pyapiary/tests/test_flashpoint/test_unit_flashpoint.py +45 -0
- pyapiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml +87 -0
- pyapiary/tests/test_generic/test_integration_generic_connector.py +12 -0
- pyapiary/tests/test_generic/test_unit_async_generic_connector.py +54 -0
- pyapiary/tests/test_generic/test_unit_generic_connector.py +34 -0
- pyapiary/tests/test_ipqs/__init__.py +0 -0
- pyapiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml +64 -0
- pyapiary/tests/test_ipqs/test_integration_ipqs.py +13 -0
- pyapiary/tests/test_ipqs/test_unit_async_ipqs.py +53 -0
- pyapiary/tests/test_ipqs/test_unit_ipqs.py +45 -0
- pyapiary/tests/test_mongodb/test_unit_async_mongo.py +109 -0
- pyapiary/tests/test_mongodb/test_unit_mongo.py +219 -0
- pyapiary/tests/test_odbc/test_unit_odbc.py +82 -0
- pyapiary/tests/test_splunk/test_unit_splunk.py +56 -0
- pyapiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml +1870 -0
- pyapiary/tests/test_spycloud/test_integration_spycloud.py +12 -0
- pyapiary/tests/test_spycloud/test_unit_async_spycloud.py +44 -0
- pyapiary/tests/test_spycloud/test_unit_spycloud.py +46 -0
- pyapiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml +68 -0
- pyapiary/tests/test_twilio/test_integration_twilio.py +14 -0
- pyapiary/tests/test_twilio/test_unit_async_twilio.py +34 -0
- pyapiary/tests/test_twilio/test_unit_twilio.py +45 -0
- pyapiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml +279 -0
- pyapiary/tests/test_urlscan/test_integration_urlscan.py +12 -0
- pyapiary/tests/test_urlscan/test_unit_async_urlscan.py +49 -0
- pyapiary/tests/test_urlscan/test_unit_urlscan.py +39 -0
- pyapiary-2.0.0.dist-info/METADATA +435 -0
- pyapiary-2.0.0.dist-info/RECORD +54 -0
- pyapiary-2.0.0.dist-info/WHEEL +4 -0
pyapiary/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# API connectors
|
|
2
|
+
from pyapiary.api_connectors import (
|
|
3
|
+
urlscan,
|
|
4
|
+
spycloud,
|
|
5
|
+
twilio,
|
|
6
|
+
flashpoint,
|
|
7
|
+
ipqs,
|
|
8
|
+
generic,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# DBMS connectors
|
|
12
|
+
from pyapiary.dbms_connectors import (
|
|
13
|
+
elasticsearch,
|
|
14
|
+
mongo,
|
|
15
|
+
odbc,
|
|
16
|
+
splunk
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Export the modules and re-exports
|
|
20
|
+
__all__ = [
|
|
21
|
+
"elasticsearch",
|
|
22
|
+
"flashpoint",
|
|
23
|
+
"generic",
|
|
24
|
+
"ipqs",
|
|
25
|
+
"mongo",
|
|
26
|
+
"odbc",
|
|
27
|
+
"splunk",
|
|
28
|
+
"spycloud",
|
|
29
|
+
"twilio",
|
|
30
|
+
"urlscan",
|
|
31
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from httpx import Auth
|
|
3
|
+
from typing import Optional, Dict, Any, Union, Iterable, Callable, ParamSpec, TypeVar, Type
|
|
4
|
+
from tenacity import retry, stop_after_attempt, wait_exponential, RetryError, retry_if_exception, AsyncRetrying
|
|
5
|
+
from pyapiary.helpers import setup_logger, combine_env_configs
|
|
6
|
+
from functools import wraps
|
|
7
|
+
import inspect
|
|
8
|
+
import os
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
P = ParamSpec("P")
|
|
13
|
+
R = TypeVar("R")
|
|
14
|
+
|
|
15
|
+
def log_method_call(func: Callable[P, R]) -> Callable[P, R]:
|
|
16
|
+
@wraps(func)
|
|
17
|
+
def wrapper(self, *args, **kwargs):
|
|
18
|
+
caller = func.__name__
|
|
19
|
+
sig = inspect.signature(func)
|
|
20
|
+
bound = sig.bind(self, *args, **kwargs)
|
|
21
|
+
bound.apply_defaults()
|
|
22
|
+
query_value = bound.arguments.get("query")
|
|
23
|
+
self._log(f"{caller} called with query: {query_value}")
|
|
24
|
+
return func(self, *args, **kwargs)
|
|
25
|
+
return wrapper
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bubble_broker_init_signature(*, exclude: Iterable[str] = ("base_url",)):
|
|
29
|
+
"""
|
|
30
|
+
Class decorator that augments a connector subclass' __init__ signature with
|
|
31
|
+
parameters from Broker.__init__ for better IDE/tab-completion hints.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
from pyapiary.api_connectors.broker import Broker, bubble_broker_init_signature
|
|
35
|
+
|
|
36
|
+
@bubble_broker_init_signature()
|
|
37
|
+
class MyConnector(Broker):
|
|
38
|
+
def __init__(self, api_key: str | None = None, **kwargs):
|
|
39
|
+
super().__init__(base_url="https://example.com", **kwargs)
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
- This affects *introspection only* (via __signature__). Runtime behavior is unchanged.
|
|
44
|
+
- Subclass-specific parameters remain first (e.g., api_key), followed by Broker params.
|
|
45
|
+
- `base_url` is excluded by default since subclasses set it themselves.
|
|
46
|
+
- The subclass' **kwargs (if present) is preserved at the end so httpx.Client kwargs
|
|
47
|
+
can still be passed through.
|
|
48
|
+
"""
|
|
49
|
+
def _decorate(cls):
|
|
50
|
+
sub_init = cls.__init__
|
|
51
|
+
broker_init = Broker.__init__
|
|
52
|
+
|
|
53
|
+
sub_sig = inspect.signature(sub_init)
|
|
54
|
+
broker_sig = inspect.signature(broker_init)
|
|
55
|
+
|
|
56
|
+
new_params = []
|
|
57
|
+
saw_var_kw = None
|
|
58
|
+
|
|
59
|
+
# Keep subclass params first; remember its **kwargs if present
|
|
60
|
+
for p in sub_sig.parameters.values():
|
|
61
|
+
if p.kind is inspect.Parameter.VAR_KEYWORD:
|
|
62
|
+
saw_var_kw = p
|
|
63
|
+
else:
|
|
64
|
+
new_params.append(p)
|
|
65
|
+
|
|
66
|
+
present = {p.name for p in new_params}
|
|
67
|
+
|
|
68
|
+
# Append Broker params (skip self, excluded, already-present, and **kwargs)
|
|
69
|
+
for name, p in list(broker_sig.parameters.items())[1:]:
|
|
70
|
+
if name in exclude or name in present:
|
|
71
|
+
continue
|
|
72
|
+
if p.kind is inspect.Parameter.VAR_KEYWORD:
|
|
73
|
+
continue
|
|
74
|
+
new_params.append(p)
|
|
75
|
+
|
|
76
|
+
# Re-append subclass **kwargs (or add a generic one to keep flexibility)
|
|
77
|
+
if saw_var_kw is not None:
|
|
78
|
+
new_params.append(saw_var_kw)
|
|
79
|
+
else:
|
|
80
|
+
new_params.append(
|
|
81
|
+
inspect.Parameter(
|
|
82
|
+
"client_kwargs",
|
|
83
|
+
kind=inspect.Parameter.VAR_KEYWORD,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
cls.__init__.__signature__ = inspect.Signature(parameters=new_params)
|
|
88
|
+
return cls
|
|
89
|
+
|
|
90
|
+
return _decorate
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SharedConnectorBase:
|
|
94
|
+
"""
|
|
95
|
+
Shared base class for Broker and AsyncBroker.
|
|
96
|
+
Houses reusable logic (constructor, logging, proxy config, retry predicate).
|
|
97
|
+
"""
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
base_url: str,
|
|
101
|
+
headers: Optional[Dict[str, str]] = None,
|
|
102
|
+
enable_logging: bool = False,
|
|
103
|
+
enable_backoff: bool = False,
|
|
104
|
+
timeout: int = 10,
|
|
105
|
+
load_env_vars: bool = False,
|
|
106
|
+
trust_env: bool = True,
|
|
107
|
+
proxy: Optional[str] = None,
|
|
108
|
+
mounts: Optional[Dict[str, httpx.HTTPTransport]] = None,
|
|
109
|
+
**client_kwargs,
|
|
110
|
+
):
|
|
111
|
+
self.base_url = base_url.rstrip('/')
|
|
112
|
+
self.logger = setup_logger(self.__class__.__name__) if enable_logging else None
|
|
113
|
+
self.enable_backoff = enable_backoff
|
|
114
|
+
self.timeout = timeout
|
|
115
|
+
self.headers = headers or {}
|
|
116
|
+
self.trust_env = trust_env
|
|
117
|
+
self.proxy = proxy
|
|
118
|
+
self.mounts = mounts
|
|
119
|
+
self.env_config = combine_env_configs() if load_env_vars else {}
|
|
120
|
+
self._client_kwargs = dict(client_kwargs) if client_kwargs else {}
|
|
121
|
+
|
|
122
|
+
def _log(self, message: str):
|
|
123
|
+
if self.logger:
|
|
124
|
+
self.logger.info(message)
|
|
125
|
+
|
|
126
|
+
def _collect_proxy_config(self) -> tuple[Optional[str], Optional[Dict[str, httpx.HTTPTransport]]]:
|
|
127
|
+
source_env: Optional[Dict[str, str]] = None
|
|
128
|
+
if isinstance(self.env_config, dict) and len(self.env_config) > 0:
|
|
129
|
+
source_env = {k: v for k, v in self.env_config.items() if isinstance(k, str) and isinstance(v, str)}
|
|
130
|
+
elif self.trust_env:
|
|
131
|
+
source_env = dict(os.environ)
|
|
132
|
+
else:
|
|
133
|
+
return None, None
|
|
134
|
+
|
|
135
|
+
def _get(key: str) -> Optional[str]:
|
|
136
|
+
return source_env.get(key) or source_env.get(key.lower())
|
|
137
|
+
|
|
138
|
+
all_proxy = _get("ALL_PROXY")
|
|
139
|
+
http_proxy = _get("HTTP_PROXY")
|
|
140
|
+
https_proxy = _get("HTTPS_PROXY")
|
|
141
|
+
|
|
142
|
+
if http_proxy and https_proxy and http_proxy != https_proxy:
|
|
143
|
+
return None, {
|
|
144
|
+
"http://": httpx.HTTPTransport(proxy=http_proxy),
|
|
145
|
+
"https://": httpx.HTTPTransport(proxy=https_proxy),
|
|
146
|
+
}
|
|
147
|
+
single = all_proxy or https_proxy or http_proxy
|
|
148
|
+
if single:
|
|
149
|
+
return single, None
|
|
150
|
+
return None, None
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _default_retry_exc(exc: BaseException) -> bool:
|
|
154
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
155
|
+
r = exc.response
|
|
156
|
+
if r is not None:
|
|
157
|
+
return r.status_code == 429 or 500 <= r.status_code < 600
|
|
158
|
+
return isinstance(exc, (
|
|
159
|
+
httpx.ConnectError,
|
|
160
|
+
httpx.ReadTimeout,
|
|
161
|
+
httpx.WriteError,
|
|
162
|
+
httpx.RemoteProtocolError,
|
|
163
|
+
httpx.PoolTimeout,
|
|
164
|
+
))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Broker(SharedConnectorBase):
|
|
168
|
+
"""
|
|
169
|
+
A base HTTP client that provides structured request handling, logging, retries, and optional environment config loading.
|
|
170
|
+
Designed to be inherited by specific API connector classes.
|
|
171
|
+
"""
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
base_url: str,
|
|
175
|
+
headers: Optional[Dict[str, str]] = None,
|
|
176
|
+
enable_logging: bool = False,
|
|
177
|
+
enable_backoff: bool = False,
|
|
178
|
+
timeout: int = 10,
|
|
179
|
+
load_env_vars: bool = False,
|
|
180
|
+
trust_env: bool = True,
|
|
181
|
+
proxy: Optional[str] = None,
|
|
182
|
+
mounts: Optional[Dict[str, httpx.HTTPTransport]] = None,
|
|
183
|
+
**client_kwargs,
|
|
184
|
+
):
|
|
185
|
+
super().__init__(
|
|
186
|
+
base_url=base_url,
|
|
187
|
+
headers=headers,
|
|
188
|
+
enable_logging=enable_logging,
|
|
189
|
+
enable_backoff=enable_backoff,
|
|
190
|
+
timeout=timeout,
|
|
191
|
+
load_env_vars=load_env_vars,
|
|
192
|
+
trust_env=trust_env,
|
|
193
|
+
proxy=proxy,
|
|
194
|
+
mounts=mounts,
|
|
195
|
+
**client_kwargs,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
client_options = dict(self._client_kwargs)
|
|
199
|
+
client_options.pop("timeout", None)
|
|
200
|
+
|
|
201
|
+
client_args = {
|
|
202
|
+
"timeout": self.timeout,
|
|
203
|
+
"trust_env": self.trust_env,
|
|
204
|
+
**client_options,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if self.mounts:
|
|
208
|
+
client_args["mounts"] = self.mounts
|
|
209
|
+
elif self.proxy:
|
|
210
|
+
client_args["proxy"] = self.proxy
|
|
211
|
+
else:
|
|
212
|
+
env_proxy, env_mounts = self._collect_proxy_config()
|
|
213
|
+
if env_mounts:
|
|
214
|
+
self.mounts = env_mounts
|
|
215
|
+
client_args["mounts"] = self.mounts
|
|
216
|
+
elif env_proxy:
|
|
217
|
+
self.proxy = env_proxy
|
|
218
|
+
client_args["proxy"] = self.proxy
|
|
219
|
+
elif not self.trust_env:
|
|
220
|
+
client_args["trust_env"] = False
|
|
221
|
+
|
|
222
|
+
self.session = httpx.Client(**client_args)
|
|
223
|
+
|
|
224
|
+
def __enter__(self) -> "Broker":
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __exit__(
|
|
228
|
+
self,
|
|
229
|
+
exc_type: Optional[Type[BaseException]],
|
|
230
|
+
exc: Optional[BaseException],
|
|
231
|
+
tb: Optional[TracebackType],
|
|
232
|
+
) -> None:
|
|
233
|
+
self.session.close()
|
|
234
|
+
|
|
235
|
+
def _make_request(
|
|
236
|
+
self,
|
|
237
|
+
method: str,
|
|
238
|
+
endpoint: str,
|
|
239
|
+
params: Optional[Dict[str, Any]] = None,
|
|
240
|
+
json: Optional[Dict[str, Any]] = None,
|
|
241
|
+
auth: Optional[Union[tuple, Auth]] = None,
|
|
242
|
+
headers: Optional[Dict[str, str]] = None,
|
|
243
|
+
retry_kwargs: Optional[Dict[str, Any]] = None,
|
|
244
|
+
**request_kwargs,
|
|
245
|
+
) -> httpx.Response:
|
|
246
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
247
|
+
|
|
248
|
+
def do_request() -> httpx.Response:
|
|
249
|
+
resp = self.session.request(
|
|
250
|
+
method=method,
|
|
251
|
+
url=url,
|
|
252
|
+
headers=headers or self.headers,
|
|
253
|
+
params=params,
|
|
254
|
+
json=json,
|
|
255
|
+
auth=auth,
|
|
256
|
+
**request_kwargs,
|
|
257
|
+
)
|
|
258
|
+
resp.raise_for_status()
|
|
259
|
+
return resp
|
|
260
|
+
|
|
261
|
+
call = do_request
|
|
262
|
+
if self.enable_backoff:
|
|
263
|
+
rk = dict(retry_kwargs or {})
|
|
264
|
+
if "retry" not in rk:
|
|
265
|
+
rk["retry"] = retry_if_exception(self._default_retry_exc)
|
|
266
|
+
if "stop" not in rk:
|
|
267
|
+
rk["stop"] = stop_after_attempt(3)
|
|
268
|
+
if "wait" not in rk:
|
|
269
|
+
rk["wait"] = wait_exponential(multiplier=1, min=2, max=10)
|
|
270
|
+
call = retry(reraise=True, **rk)(do_request)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
return call()
|
|
274
|
+
except RetryError as re:
|
|
275
|
+
last = re.last_attempt.exception()
|
|
276
|
+
self._log(f"Retry failed: {last}")
|
|
277
|
+
raise
|
|
278
|
+
except httpx.HTTPStatusError as he:
|
|
279
|
+
self._log(f"HTTP error: {he}")
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
283
|
+
return self._make_request("GET", endpoint, params=params, **kwargs)
|
|
284
|
+
|
|
285
|
+
def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
286
|
+
return self._make_request("POST", endpoint, json=json, **kwargs)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class AsyncBroker(SharedConnectorBase):
|
|
290
|
+
"""
|
|
291
|
+
Async HTTP client connector. Provides async _make_request, get, and post.
|
|
292
|
+
"""
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
base_url: str,
|
|
296
|
+
headers: Optional[Dict[str, str]] = None,
|
|
297
|
+
enable_logging: bool = False,
|
|
298
|
+
enable_backoff: bool = False,
|
|
299
|
+
timeout: int = 10,
|
|
300
|
+
load_env_vars: bool = False,
|
|
301
|
+
trust_env: bool = True,
|
|
302
|
+
proxy: Optional[str] = None,
|
|
303
|
+
mounts: Optional[Dict[str, httpx.HTTPTransport]] = None,
|
|
304
|
+
**client_kwargs,
|
|
305
|
+
):
|
|
306
|
+
super().__init__(
|
|
307
|
+
base_url=base_url,
|
|
308
|
+
headers=headers,
|
|
309
|
+
enable_logging=enable_logging,
|
|
310
|
+
enable_backoff=enable_backoff,
|
|
311
|
+
timeout=timeout,
|
|
312
|
+
load_env_vars=load_env_vars,
|
|
313
|
+
trust_env=trust_env,
|
|
314
|
+
proxy=proxy,
|
|
315
|
+
mounts=mounts,
|
|
316
|
+
**client_kwargs,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if self.mounts:
|
|
320
|
+
raise ValueError("The 'mounts' parameter is not supported in AsyncBroker but "
|
|
321
|
+
"you can still use 'proxy' or 'trust_env' if 'HTTP_PROXY' or "
|
|
322
|
+
"'HTTPS_PROXY' are in your system environment variables ")
|
|
323
|
+
|
|
324
|
+
resolved_proxy = self.proxy or self._collect_proxy_config()[0]
|
|
325
|
+
|
|
326
|
+
self.session = httpx.AsyncClient(
|
|
327
|
+
timeout=self.timeout,
|
|
328
|
+
proxy=resolved_proxy,
|
|
329
|
+
trust_env=self.trust_env,
|
|
330
|
+
**self._client_kwargs,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
async def __aenter__(self) -> "AsyncBroker":
|
|
334
|
+
return self
|
|
335
|
+
|
|
336
|
+
async def __aexit__(
|
|
337
|
+
self,
|
|
338
|
+
exc_type: Optional[Type[BaseException]],
|
|
339
|
+
exc: Optional[BaseException],
|
|
340
|
+
tb: Optional[TracebackType],
|
|
341
|
+
) -> None:
|
|
342
|
+
await self.session.aclose()
|
|
343
|
+
|
|
344
|
+
async def _make_request(
|
|
345
|
+
self,
|
|
346
|
+
method: str,
|
|
347
|
+
endpoint: str,
|
|
348
|
+
params: Optional[Dict[str, Any]] = None,
|
|
349
|
+
json: Optional[Dict[str, Any]] = None,
|
|
350
|
+
auth: Optional[Union[tuple, Auth]] = None,
|
|
351
|
+
headers: Optional[Dict[str, str]] = None,
|
|
352
|
+
retry_kwargs: Optional[Dict[str, Any]] = None,
|
|
353
|
+
**request_kwargs,
|
|
354
|
+
) -> httpx.Response:
|
|
355
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
356
|
+
|
|
357
|
+
async def do_request() -> httpx.Response:
|
|
358
|
+
resp = await self.session.request(
|
|
359
|
+
method=method,
|
|
360
|
+
url=url,
|
|
361
|
+
headers=headers or self.headers,
|
|
362
|
+
params=params,
|
|
363
|
+
json=json,
|
|
364
|
+
auth=auth,
|
|
365
|
+
**request_kwargs,
|
|
366
|
+
)
|
|
367
|
+
resp.raise_for_status()
|
|
368
|
+
return resp
|
|
369
|
+
|
|
370
|
+
call = do_request
|
|
371
|
+
if self.enable_backoff:
|
|
372
|
+
|
|
373
|
+
rk = dict(retry_kwargs or {})
|
|
374
|
+
retry_pred = rk.get("retry", retry_if_exception(self._default_retry_exc))
|
|
375
|
+
stop_cond = rk.get("stop", stop_after_attempt(3))
|
|
376
|
+
wait_cond = rk.get("wait", wait_exponential(multiplier=1, min=2, max=10))
|
|
377
|
+
|
|
378
|
+
async def retry_wrapper():
|
|
379
|
+
async for attempt in AsyncRetrying(reraise=True, retry=retry_pred, stop=stop_cond, wait=wait_cond):
|
|
380
|
+
with attempt:
|
|
381
|
+
return await do_request()
|
|
382
|
+
call = retry_wrapper
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
return await call()
|
|
386
|
+
except RetryError as re:
|
|
387
|
+
last = re.last_attempt.exception()
|
|
388
|
+
self._log(f"Retry failed: {last}")
|
|
389
|
+
raise
|
|
390
|
+
except httpx.HTTPStatusError as he:
|
|
391
|
+
self._log(f"HTTP error: {he}")
|
|
392
|
+
raise
|
|
393
|
+
|
|
394
|
+
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
395
|
+
return await self._make_request("GET", endpoint, params=params, **kwargs)
|
|
396
|
+
|
|
397
|
+
async def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
398
|
+
return await self._make_request("POST", endpoint, json=json, **kwargs)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pyapiary.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call
|
|
4
|
+
|
|
5
|
+
@bubble_broker_init_signature()
|
|
6
|
+
class FlashpointConnector(Broker):
|
|
7
|
+
"""
|
|
8
|
+
FlashpointConnector provides access to various Flashpoint API search and retrieval endpoints
|
|
9
|
+
using a consistent Broker-based interface.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
api_key (str): Flashpoint API token used for bearer authentication.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
15
|
+
super().__init__(base_url="https://api.flashpoint.io", **kwargs)
|
|
16
|
+
self.api_key = api_key or self.env_config.get("FLASHPOINT_API_KEY")
|
|
17
|
+
if not self.api_key:
|
|
18
|
+
raise ValueError("FLASHPOINT_API_KEY is required")
|
|
19
|
+
self.headers.update({
|
|
20
|
+
"accept": "application/json",
|
|
21
|
+
"content-type": "application/json",
|
|
22
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
@log_method_call
|
|
26
|
+
def search_communities(self, query: str, **kwargs) -> httpx.Response:
|
|
27
|
+
"""
|
|
28
|
+
Search Flashpoint communities data.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
query (str): The search string used in the API query.
|
|
32
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
33
|
+
"""
|
|
34
|
+
return self.post("/sources/v2/communities", json={"query": query, **kwargs})
|
|
35
|
+
|
|
36
|
+
@log_method_call
|
|
37
|
+
def search_fraud(self, query: str, **kwargs) -> httpx.Response:
|
|
38
|
+
"""
|
|
39
|
+
Search Flashpoint fraud datasets.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
query (str): The search string used in the API query.
|
|
43
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
44
|
+
"""
|
|
45
|
+
return self.post("/sources/v2/fraud", json={"query": query, **kwargs})
|
|
46
|
+
|
|
47
|
+
@log_method_call
|
|
48
|
+
def search_marketplaces(self, query: str, **kwargs) -> httpx.Response:
|
|
49
|
+
"""
|
|
50
|
+
Search Flashpoint marketplace datasets.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
query (str): The search string used in the API query.
|
|
54
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
55
|
+
"""
|
|
56
|
+
return self.post("/sources/v2/markets", json={"query": query, **kwargs})
|
|
57
|
+
|
|
58
|
+
@log_method_call
|
|
59
|
+
def search_media(self, query: str, **kwargs) -> httpx.Response:
|
|
60
|
+
"""
|
|
61
|
+
Search OCR-processed media from Flashpoint.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
query (str): The search string used in the API query.
|
|
65
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
66
|
+
"""
|
|
67
|
+
return self.post("/sources/v2/media", json={"query": query, **kwargs})
|
|
68
|
+
|
|
69
|
+
@log_method_call
|
|
70
|
+
def get_media_object(self, query: str, **kwargs) -> httpx.Response:
|
|
71
|
+
"""
|
|
72
|
+
Retrieve metadata for a specific media object.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
query (str): The media_id of the object to retrieve.
|
|
76
|
+
**kwargs: Additional request options.
|
|
77
|
+
"""
|
|
78
|
+
return self.get(f"/sources/v2/media/{query}")
|
|
79
|
+
|
|
80
|
+
@log_method_call
|
|
81
|
+
def get_media_image(self, query: str, **kwargs) -> httpx.Response:
|
|
82
|
+
"""
|
|
83
|
+
Download image asset by storage_uri.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
query (str): The storage_uri (asset_id) of the image to download.
|
|
87
|
+
**kwargs: Additional request options.
|
|
88
|
+
"""
|
|
89
|
+
safe_headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
90
|
+
return self.get("/sources/v1/media/", headers=safe_headers, params={"asset_id": query})
|
|
91
|
+
|
|
92
|
+
@log_method_call
|
|
93
|
+
def search_checks(self, query: str, **kwargs) -> httpx.Response:
|
|
94
|
+
"""
|
|
95
|
+
Search Flashpoint fraud check datasets.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
query (str): The search string used in the API query.
|
|
99
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
100
|
+
"""
|
|
101
|
+
return self.post("/sources/v2/fraud/checks", json={"query": query, **kwargs})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Async version of FlashpointConnector
|
|
105
|
+
@bubble_broker_init_signature()
|
|
106
|
+
class AsyncFlashpointConnector(AsyncBroker):
|
|
107
|
+
"""
|
|
108
|
+
AsyncFlashpointConnector provides async access to Flashpoint API endpoints.
|
|
109
|
+
"""
|
|
110
|
+
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
111
|
+
super().__init__(base_url="https://api.flashpoint.io", **kwargs)
|
|
112
|
+
self.api_key = api_key or self.env_config.get("FLASHPOINT_API_KEY")
|
|
113
|
+
if not self.api_key:
|
|
114
|
+
raise ValueError("FLASHPOINT_API_KEY is required")
|
|
115
|
+
self.headers.update({
|
|
116
|
+
"accept": "application/json",
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
@log_method_call
|
|
122
|
+
async def search_communities(self, query: str, **kwargs) -> httpx.Response:
|
|
123
|
+
"""
|
|
124
|
+
Search Flashpoint communities data.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
query (str): The search string used in the API query.
|
|
128
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
129
|
+
"""
|
|
130
|
+
return await self.post("/sources/v2/communities", json={"query": query, **kwargs})
|
|
131
|
+
|
|
132
|
+
@log_method_call
|
|
133
|
+
async def search_fraud(self, query: str, **kwargs) -> httpx.Response:
|
|
134
|
+
"""
|
|
135
|
+
Search Flashpoint fraud datasets.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
query (str): The search string used in the API query.
|
|
139
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
140
|
+
"""
|
|
141
|
+
return await self.post("/sources/v2/fraud", json={"query": query, **kwargs})
|
|
142
|
+
|
|
143
|
+
@log_method_call
|
|
144
|
+
async def search_marketplaces(self, query: str, **kwargs) -> httpx.Response:
|
|
145
|
+
"""
|
|
146
|
+
Search Flashpoint marketplace datasets.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
query (str): The search string used in the API query.
|
|
150
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
151
|
+
"""
|
|
152
|
+
return await self.post("/sources/v2/markets", json={"query": query, **kwargs})
|
|
153
|
+
|
|
154
|
+
@log_method_call
|
|
155
|
+
async def search_media(self, query: str, **kwargs) -> httpx.Response:
|
|
156
|
+
"""
|
|
157
|
+
Search OCR-processed media from Flashpoint.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
query (str): The search string used in the API query.
|
|
161
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
162
|
+
"""
|
|
163
|
+
return await self.post("/sources/v2/media", json={"query": query, **kwargs})
|
|
164
|
+
|
|
165
|
+
@log_method_call
|
|
166
|
+
async def get_media_object(self, query: str) -> httpx.Response:
|
|
167
|
+
"""
|
|
168
|
+
Retrieve metadata for a specific media object.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
query (str): The media_id of the object to retrieve.
|
|
172
|
+
"""
|
|
173
|
+
return await self.get(f"/sources/v2/media/{query}")
|
|
174
|
+
|
|
175
|
+
@log_method_call
|
|
176
|
+
async def get_media_image(self, query: str) -> httpx.Response:
|
|
177
|
+
"""
|
|
178
|
+
Download image asset by storage_uri.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
query (str): The storage_uri (asset_id) of the image to download.
|
|
182
|
+
"""
|
|
183
|
+
safe_headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
184
|
+
return await self.get("/sources/v1/media/", headers=safe_headers, params={"asset_id": query})
|
|
185
|
+
|
|
186
|
+
@log_method_call
|
|
187
|
+
async def search_checks(self, query: str, **kwargs) -> httpx.Response:
|
|
188
|
+
"""
|
|
189
|
+
Search Flashpoint fraud check datasets asynchronously.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
query (str): The search string used in the API query.
|
|
193
|
+
**kwargs: Additional query logic per the Flashpoint API documentation.
|
|
194
|
+
"""
|
|
195
|
+
return await self.post("/sources/v2/fraud/checks", json={"query": query, **kwargs})
|