From 9e3aae53de661c32c52d04b8d26ca6268c1b812b Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:36:47 -0500 Subject: [PATCH 1/9] update packages, bump version, update lock, update env vars --- .env.example | 4 ++-- README.md | 28 ++++++++++++++-------------- poetry.lock | 7 +++---- pyproject.toml | 10 ++++++---- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index 78bcb7a..00336cc 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ ################# # ENVIRONMENTAL # ################# -PPP_HTTP_PROXY= -PPP_HTTPS_PROXY= +HTTP_PROXY= +HTTPS_PROXY= VERIFY_SSL= ############## diff --git a/README.md b/README.md index ba69440..f35d05b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ppp_connectors +# APIary A clean, modular set of Python connectors and utilities for working with both **APIs** and **DBMS backends**, unified by a centralized `Broker` abstraction and a consistent interface. Designed for easy testing, code reuse, and plug-and-play extensibility. @@ -76,7 +76,7 @@ Each API connector has both a sync and async version (e.g., `URLScanConnector` a ### 🌐 Sync Example (URLScan) ``` python -from ppp_connectors.api_connectors.urlscan import URLScanConnector +from apiary.api_connectors.urlscan import URLScanConnector scanner = URLScanConnector(load_env_vars=True) result = scanner.scan(url="https://example.com") @@ -87,7 +87,7 @@ print(result.json()) ### ⚡ Async Example (URLScan) ``` python import asyncio -from ppp_connectors.api_connectors.urlscan import AsyncURLScanConnector +from apiary.api_connectors.urlscan import AsyncURLScanConnector async def main(): scanner = AsyncURLScanConnector(load_env_vars=True) @@ -135,7 +135,7 @@ API connectors inherit from the `Broker` class and support flexible proxy config *Using a single proxy:* ```python -from ppp_connectors.api_connectors.urlscan import URLScanConnector +from apiary.api_connectors.urlscan import URLScanConnector conn = URLScanConnector(proxy="http://myproxy:8080") ``` @@ -196,7 +196,7 @@ Note: `query(...)` is deprecated in favor of `find(filter=..., projection=..., b Sync connector ```python -from ppp_connectors.dbms_connectors.mongo import MongoConnector +from apiary.dbms_connectors.mongo import MongoConnector # Recommended: use as a context manager (auto-closes) with MongoConnector( @@ -237,7 +237,7 @@ finally: Async connector ```python import asyncio -from ppp_connectors.dbms_connectors.mongo_async import AsyncMongoConnector +from apiary.dbms_connectors.mongo_async import AsyncMongoConnector async def main(): async with AsyncMongoConnector( @@ -266,7 +266,7 @@ asyncio.run(main()) ```python # The query method returns a generator; use list() or iterate to access results -from ppp_connectors.dbms_connectors.elasticsearch import ElasticsearchConnector +from apiary.dbms_connectors.elasticsearch import ElasticsearchConnector conn = ElasticsearchConnector(["http://localhost:9200"]) results = list(conn.query("my-index", {"query": {"match_all": {}}})) @@ -279,7 +279,7 @@ for doc in results: For automatic connection handling, use `ODBCConnector` as a context manager ```python -from ppp_connectors.dbms_connectors.odbc import ODBCConnector +from apiary.dbms_connectors.odbc import ODBCConnector with ODBCConnector("DSN=PostgresLocal;UID=postgres;PWD=postgres") as db: rows = conn.query("SELECT * FROM my_table") @@ -289,7 +289,7 @@ with ODBCConnector("DSN=PostgresLocal;UID=postgres;PWD=postgres") as db: If you'd like to keep manual control, you can still use the `.close()` method ```python -from ppp_connectors.dbms_connectors.odbc import ODBCConnector +from apiary.dbms_connectors.odbc import ODBCConnector conn = ODBCConnector("DSN=PostgresLocal;UID=postgres;PWD=postgres") rows = conn.query("SELECT * FROM my_table") @@ -300,7 +300,7 @@ conn.close() ### Splunk ```python -from ppp_connectors.dbms_connectors.splunk import SplunkConnector +from apiary.dbms_connectors.splunk import SplunkConnector conn = SplunkConnector("localhost", 8089, "admin", "admin123", scheme="https", verify=False) results = conn.query("search index=_internal | head 5") @@ -344,8 +344,8 @@ markers = To add a new connector: 1. **Module**: Place your module in: - - `ppp_connectors/api_connectors/` for API-based integrations - - `ppp_connectors/dbms_connectors/` for database-style connectors + - `src/apiary/api_connectors/` for API-based integrations + - `src/apiary/dbms_connectors/` for database-style connectors 2. **Base class**: - Use the `Broker` class for APIs @@ -371,8 +371,8 @@ To add a new connector: ## 🛠️ Dev Environment ```bash -git clone https://github.com/FineYoungCannibals/ppp_connectors.git -cd ppp_connectors +git clone https://github.com/robd518/apiary.git +cd apiary cp .env.example .env diff --git a/poetry.lock b/poetry.lock index dbb99cb..c02077d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -679,10 +679,9 @@ zstd = ["zstandard"] name = "pyodbc" version = "5.3.0" description = "DB API module for ODBC" -optional = true +optional = false python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"odbc\"" files = [ {file = "pyodbc-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5"}, {file = "pyodbc-5.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2"}, @@ -1373,9 +1372,9 @@ multidict = ">=4.0" propcache = ">=0.2.1" [extras] -odbc = ["pyodbc"] +odbc = [] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "0d4e179b4e600b3f627bfe0514b4147ee3f17b5aaaf2b7fb374fe4befe25abba" +content-hash = "7c335bd1bffc7815b9c7e4c7c8869fb9dc389befdd7c3ab841eea8ffe799284d" diff --git a/pyproject.toml b/pyproject.toml index f024178..e4dd835 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,12 @@ [tool.poetry] -name = "ppp-connectors" -packages = [{ include = "ppp_connectors" }] -version = "1.1.13" +name = "apiary" +packages = [{ include = "apiary", from = "src" }] +version = "2.0.0" description = "A simple, lightweight set of connectors and functions to various APIs and DBMSs, controlled by a central broker." authors = ["Rob D'Aveta "] readme = "README.md" +homepage = "https://github.com/robd518/apiary" +repository = "https://github.com/robd518/apiary" [tool.poetry.dependencies] python = "^3.10" @@ -12,9 +14,9 @@ python-dotenv = "^1.1.1" pymongo = "^4.15.0" elasticsearch = "^9.1.1" splunk-sdk = "^2.1.1" -pyodbc = { version = "^5.2.0", optional = true } httpx = "^0.28.1" tenacity = "^9.1.2" +pyodbc = "^5.3.0" [tool.poetry.extras] odbc = ["pyodbc"] From 440e913e040d6874580e94c22e7b9d176dd2cb02 Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:37:57 -0500 Subject: [PATCH 2/9] update imports --- src/apiary/__init__.py | 31 ++ src/apiary/api_connectors/__init__.py | 0 src/apiary/api_connectors/broker.py | 398 ++++++++++++++++++++ src/apiary/api_connectors/flashpoint.py | 195 ++++++++++ src/apiary/api_connectors/generic.py | 105 ++++++ src/apiary/api_connectors/ipqs.py | 68 ++++ src/apiary/api_connectors/spycloud.py | 207 ++++++++++ src/apiary/api_connectors/twilio.py | 114 ++++++ src/apiary/api_connectors/urlscan.py | 148 ++++++++ src/apiary/dbms_connectors/__init__.py | 0 src/apiary/dbms_connectors/elasticsearch.py | 143 +++++++ src/apiary/dbms_connectors/mongo.py | 390 +++++++++++++++++++ src/apiary/dbms_connectors/mongo_async.py | 323 ++++++++++++++++ src/apiary/dbms_connectors/odbc.py | 110 ++++++ src/apiary/dbms_connectors/splunk.py | 131 +++++++ 15 files changed, 2363 insertions(+) create mode 100644 src/apiary/__init__.py create mode 100644 src/apiary/api_connectors/__init__.py create mode 100644 src/apiary/api_connectors/broker.py create mode 100644 src/apiary/api_connectors/flashpoint.py create mode 100644 src/apiary/api_connectors/generic.py create mode 100644 src/apiary/api_connectors/ipqs.py create mode 100644 src/apiary/api_connectors/spycloud.py create mode 100644 src/apiary/api_connectors/twilio.py create mode 100644 src/apiary/api_connectors/urlscan.py create mode 100644 src/apiary/dbms_connectors/__init__.py create mode 100644 src/apiary/dbms_connectors/elasticsearch.py create mode 100644 src/apiary/dbms_connectors/mongo.py create mode 100644 src/apiary/dbms_connectors/mongo_async.py create mode 100644 src/apiary/dbms_connectors/odbc.py create mode 100644 src/apiary/dbms_connectors/splunk.py diff --git a/src/apiary/__init__.py b/src/apiary/__init__.py new file mode 100644 index 0000000..f0e1e27 --- /dev/null +++ b/src/apiary/__init__.py @@ -0,0 +1,31 @@ +# API connectors +from apiary.api_connectors import ( + urlscan, + spycloud, + twilio, + flashpoint, + ipqs, + generic, +) + +# DBMS connectors +from apiary.dbms_connectors import ( + elasticsearch, + mongo, + odbc, + splunk +) + +# Export the modules and re-exports +__all__ = [ + "elasticsearch", + "flashpoint", + "generic", + "ipqs", + "mongo", + "odbc", + "splunk", + "spycloud", + "twilio", + "urlscan", +] \ No newline at end of file diff --git a/src/apiary/api_connectors/__init__.py b/src/apiary/api_connectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apiary/api_connectors/broker.py b/src/apiary/api_connectors/broker.py new file mode 100644 index 0000000..ea41a56 --- /dev/null +++ b/src/apiary/api_connectors/broker.py @@ -0,0 +1,398 @@ +import httpx +from httpx import Auth +from typing import Optional, Dict, Any, Union, Iterable, Callable, ParamSpec, TypeVar, Type +from tenacity import retry, stop_after_attempt, wait_exponential, RetryError, retry_if_exception, AsyncRetrying +from apiary.helpers import setup_logger, combine_env_configs +from functools import wraps +import inspect +import os +from types import TracebackType + + +P = ParamSpec("P") +R = TypeVar("R") + +def log_method_call(func: Callable[P, R]) -> Callable[P, R]: + @wraps(func) + def wrapper(self, *args, **kwargs): + caller = func.__name__ + sig = inspect.signature(func) + bound = sig.bind(self, *args, **kwargs) + bound.apply_defaults() + query_value = bound.arguments.get("query") + self._log(f"{caller} called with query: {query_value}") + return func(self, *args, **kwargs) + return wrapper + + +def bubble_broker_init_signature(*, exclude: Iterable[str] = ("base_url",)): + """ + Class decorator that augments a connector subclass' __init__ signature with + parameters from Broker.__init__ for better IDE/tab-completion hints. + + Usage: + from apiary.api_connectors.broker import Broker, bubble_broker_init_signature + + @bubble_broker_init_signature() + class MyConnector(Broker): + def __init__(self, api_key: str | None = None, **kwargs): + super().__init__(base_url="https://example.com", **kwargs) + ... + + Notes: + - This affects *introspection only* (via __signature__). Runtime behavior is unchanged. + - Subclass-specific parameters remain first (e.g., api_key), followed by Broker params. + - `base_url` is excluded by default since subclasses set it themselves. + - The subclass' **kwargs (if present) is preserved at the end so httpx.Client kwargs + can still be passed through. + """ + def _decorate(cls): + sub_init = cls.__init__ + broker_init = Broker.__init__ + + sub_sig = inspect.signature(sub_init) + broker_sig = inspect.signature(broker_init) + + new_params = [] + saw_var_kw = None + + # Keep subclass params first; remember its **kwargs if present + for p in sub_sig.parameters.values(): + if p.kind is inspect.Parameter.VAR_KEYWORD: + saw_var_kw = p + else: + new_params.append(p) + + present = {p.name for p in new_params} + + # Append Broker params (skip self, excluded, already-present, and **kwargs) + for name, p in list(broker_sig.parameters.items())[1:]: + if name in exclude or name in present: + continue + if p.kind is inspect.Parameter.VAR_KEYWORD: + continue + new_params.append(p) + + # Re-append subclass **kwargs (or add a generic one to keep flexibility) + if saw_var_kw is not None: + new_params.append(saw_var_kw) + else: + new_params.append( + inspect.Parameter( + "client_kwargs", + kind=inspect.Parameter.VAR_KEYWORD, + ) + ) + + cls.__init__.__signature__ = inspect.Signature(parameters=new_params) + return cls + + return _decorate + + +class SharedConnectorBase: + """ + Shared base class for Broker and AsyncBroker. + Houses reusable logic (constructor, logging, proxy config, retry predicate). + """ + def __init__( + self, + base_url: str, + headers: Optional[Dict[str, str]] = None, + enable_logging: bool = False, + enable_backoff: bool = False, + timeout: int = 10, + load_env_vars: bool = False, + trust_env: bool = True, + proxy: Optional[str] = None, + mounts: Optional[Dict[str, httpx.HTTPTransport]] = None, + **client_kwargs, + ): + self.base_url = base_url.rstrip('/') + self.logger = setup_logger(self.__class__.__name__) if enable_logging else None + self.enable_backoff = enable_backoff + self.timeout = timeout + self.headers = headers or {} + self.trust_env = trust_env + self.proxy = proxy + self.mounts = mounts + self.env_config = combine_env_configs() if load_env_vars else {} + self._client_kwargs = dict(client_kwargs) if client_kwargs else {} + + def _log(self, message: str): + if self.logger: + self.logger.info(message) + + def _collect_proxy_config(self) -> tuple[Optional[str], Optional[Dict[str, httpx.HTTPTransport]]]: + source_env: Optional[Dict[str, str]] = None + if isinstance(self.env_config, dict) and len(self.env_config) > 0: + source_env = {k: v for k, v in self.env_config.items() if isinstance(k, str) and isinstance(v, str)} + elif self.trust_env: + source_env = dict(os.environ) + else: + return None, None + + def _get(key: str) -> Optional[str]: + return source_env.get(key) or source_env.get(key.lower()) + + all_proxy = _get("ALL_PROXY") + http_proxy = _get("HTTP_PROXY") + https_proxy = _get("HTTPS_PROXY") + + if http_proxy and https_proxy and http_proxy != https_proxy: + return None, { + "http://": httpx.HTTPTransport(proxy=http_proxy), + "https://": httpx.HTTPTransport(proxy=https_proxy), + } + single = all_proxy or https_proxy or http_proxy + if single: + return single, None + return None, None + + @staticmethod + def _default_retry_exc(exc: BaseException) -> bool: + if isinstance(exc, httpx.HTTPStatusError): + r = exc.response + if r is not None: + return r.status_code == 429 or 500 <= r.status_code < 600 + return isinstance(exc, ( + httpx.ConnectError, + httpx.ReadTimeout, + httpx.WriteError, + httpx.RemoteProtocolError, + httpx.PoolTimeout, + )) + + +class Broker(SharedConnectorBase): + """ + A base HTTP client that provides structured request handling, logging, retries, and optional environment config loading. + Designed to be inherited by specific API connector classes. + """ + def __init__( + self, + base_url: str, + headers: Optional[Dict[str, str]] = None, + enable_logging: bool = False, + enable_backoff: bool = False, + timeout: int = 10, + load_env_vars: bool = False, + trust_env: bool = True, + proxy: Optional[str] = None, + mounts: Optional[Dict[str, httpx.HTTPTransport]] = None, + **client_kwargs, + ): + super().__init__( + base_url=base_url, + headers=headers, + enable_logging=enable_logging, + enable_backoff=enable_backoff, + timeout=timeout, + load_env_vars=load_env_vars, + trust_env=trust_env, + proxy=proxy, + mounts=mounts, + **client_kwargs, + ) + + client_options = dict(self._client_kwargs) + client_options.pop("timeout", None) + + client_args = { + "timeout": self.timeout, + "trust_env": self.trust_env, + **client_options, + } + + if self.mounts: + client_args["mounts"] = self.mounts + elif self.proxy: + client_args["proxy"] = self.proxy + else: + env_proxy, env_mounts = self._collect_proxy_config() + if env_mounts: + self.mounts = env_mounts + client_args["mounts"] = self.mounts + elif env_proxy: + self.proxy = env_proxy + client_args["proxy"] = self.proxy + elif not self.trust_env: + client_args["trust_env"] = False + + self.session = httpx.Client(**client_args) + + def __enter__(self) -> "Broker": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + self.session.close() + + def _make_request( + self, + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + json: Optional[Dict[str, Any]] = None, + auth: Optional[Union[tuple, Auth]] = None, + headers: Optional[Dict[str, str]] = None, + retry_kwargs: Optional[Dict[str, Any]] = None, + **request_kwargs, + ) -> httpx.Response: + url = f"{self.base_url}/{endpoint.lstrip('/')}" + + def do_request() -> httpx.Response: + resp = self.session.request( + method=method, + url=url, + headers=headers or self.headers, + params=params, + json=json, + auth=auth, + **request_kwargs, + ) + resp.raise_for_status() + return resp + + call = do_request + if self.enable_backoff: + rk = dict(retry_kwargs or {}) + if "retry" not in rk: + rk["retry"] = retry_if_exception(self._default_retry_exc) + if "stop" not in rk: + rk["stop"] = stop_after_attempt(3) + if "wait" not in rk: + rk["wait"] = wait_exponential(multiplier=1, min=2, max=10) + call = retry(reraise=True, **rk)(do_request) + + try: + return call() + except RetryError as re: + last = re.last_attempt.exception() + self._log(f"Retry failed: {last}") + raise + except httpx.HTTPStatusError as he: + self._log(f"HTTP error: {he}") + raise + + def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: + return self._make_request("GET", endpoint, params=params, **kwargs) + + def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: + return self._make_request("POST", endpoint, json=json, **kwargs) + + +class AsyncBroker(SharedConnectorBase): + """ + Async HTTP client connector. Provides async _make_request, get, and post. + """ + def __init__( + self, + base_url: str, + headers: Optional[Dict[str, str]] = None, + enable_logging: bool = False, + enable_backoff: bool = False, + timeout: int = 10, + load_env_vars: bool = False, + trust_env: bool = True, + proxy: Optional[str] = None, + mounts: Optional[Dict[str, httpx.HTTPTransport]] = None, + **client_kwargs, + ): + super().__init__( + base_url=base_url, + headers=headers, + enable_logging=enable_logging, + enable_backoff=enable_backoff, + timeout=timeout, + load_env_vars=load_env_vars, + trust_env=trust_env, + proxy=proxy, + mounts=mounts, + **client_kwargs, + ) + + if self.mounts: + raise ValueError("The 'mounts' parameter is not supported in AsyncBroker but " + "you can still use 'proxy' or 'trust_env' if 'HTTP_PROXY' or " + "'HTTPS_PROXY' are in your system environment variables ") + + resolved_proxy = self.proxy or self._collect_proxy_config()[0] + + self.session = httpx.AsyncClient( + timeout=self.timeout, + proxy=resolved_proxy, + trust_env=self.trust_env, + **self._client_kwargs, + ) + + async def __aenter__(self) -> "AsyncBroker": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + await self.session.aclose() + + async def _make_request( + self, + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + json: Optional[Dict[str, Any]] = None, + auth: Optional[Union[tuple, Auth]] = None, + headers: Optional[Dict[str, str]] = None, + retry_kwargs: Optional[Dict[str, Any]] = None, + **request_kwargs, + ) -> httpx.Response: + url = f"{self.base_url}/{endpoint.lstrip('/')}" + + async def do_request() -> httpx.Response: + resp = await self.session.request( + method=method, + url=url, + headers=headers or self.headers, + params=params, + json=json, + auth=auth, + **request_kwargs, + ) + resp.raise_for_status() + return resp + + call = do_request + if self.enable_backoff: + + rk = dict(retry_kwargs or {}) + retry_pred = rk.get("retry", retry_if_exception(self._default_retry_exc)) + stop_cond = rk.get("stop", stop_after_attempt(3)) + wait_cond = rk.get("wait", wait_exponential(multiplier=1, min=2, max=10)) + + async def retry_wrapper(): + async for attempt in AsyncRetrying(reraise=True, retry=retry_pred, stop=stop_cond, wait=wait_cond): + with attempt: + return await do_request() + call = retry_wrapper + + try: + return await call() + except RetryError as re: + last = re.last_attempt.exception() + self._log(f"Retry failed: {last}") + raise + except httpx.HTTPStatusError as he: + self._log(f"HTTP error: {he}") + raise + + async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: + return await self._make_request("GET", endpoint, params=params, **kwargs) + + async def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: + return await self._make_request("POST", endpoint, json=json, **kwargs) diff --git a/src/apiary/api_connectors/flashpoint.py b/src/apiary/api_connectors/flashpoint.py new file mode 100644 index 0000000..b296b79 --- /dev/null +++ b/src/apiary/api_connectors/flashpoint.py @@ -0,0 +1,195 @@ +import httpx +from typing import Optional +from apiary.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call + +@bubble_broker_init_signature() +class FlashpointConnector(Broker): + """ + FlashpointConnector provides access to various Flashpoint API search and retrieval endpoints + using a consistent Broker-based interface. + + Attributes: + api_key (str): Flashpoint API token used for bearer authentication. + """ + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://api.flashpoint.io", **kwargs) + self.api_key = api_key or self.env_config.get("FLASHPOINT_API_KEY") + if not self.api_key: + raise ValueError("FLASHPOINT_API_KEY is required") + self.headers.update({ + "accept": "application/json", + "content-type": "application/json", + "Authorization": f"Bearer {self.api_key}", + }) + + @log_method_call + def search_communities(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint communities data. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return self.post("/sources/v2/communities", json={"query": query, **kwargs}) + + @log_method_call + def search_fraud(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint fraud datasets. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return self.post("/sources/v2/fraud", json={"query": query, **kwargs}) + + @log_method_call + def search_marketplaces(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint marketplace datasets. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return self.post("/sources/v2/markets", json={"query": query, **kwargs}) + + @log_method_call + def search_media(self, query: str, **kwargs) -> httpx.Response: + """ + Search OCR-processed media from Flashpoint. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return self.post("/sources/v2/media", json={"query": query, **kwargs}) + + @log_method_call + def get_media_object(self, query: str, **kwargs) -> httpx.Response: + """ + Retrieve metadata for a specific media object. + + Args: + query (str): The media_id of the object to retrieve. + **kwargs: Additional request options. + """ + return self.get(f"/sources/v2/media/{query}") + + @log_method_call + def get_media_image(self, query: str, **kwargs) -> httpx.Response: + """ + Download image asset by storage_uri. + + Args: + query (str): The storage_uri (asset_id) of the image to download. + **kwargs: Additional request options. + """ + safe_headers = {"Authorization": f"Bearer {self.api_key}"} + return self.get("/sources/v1/media/", headers=safe_headers, params={"asset_id": query}) + + @log_method_call + def search_checks(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint fraud check datasets. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return self.post("/sources/v2/fraud/checks", json={"query": query, **kwargs}) + + +# Async version of FlashpointConnector +@bubble_broker_init_signature() +class AsyncFlashpointConnector(AsyncBroker): + """ + AsyncFlashpointConnector provides async access to Flashpoint API endpoints. + """ + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://api.flashpoint.io", **kwargs) + self.api_key = api_key or self.env_config.get("FLASHPOINT_API_KEY") + if not self.api_key: + raise ValueError("FLASHPOINT_API_KEY is required") + self.headers.update({ + "accept": "application/json", + "content-type": "application/json", + "Authorization": f"Bearer {self.api_key}", + }) + + @log_method_call + async def search_communities(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint communities data. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return await self.post("/sources/v2/communities", json={"query": query, **kwargs}) + + @log_method_call + async def search_fraud(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint fraud datasets. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return await self.post("/sources/v2/fraud", json={"query": query, **kwargs}) + + @log_method_call + async def search_marketplaces(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint marketplace datasets. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return await self.post("/sources/v2/markets", json={"query": query, **kwargs}) + + @log_method_call + async def search_media(self, query: str, **kwargs) -> httpx.Response: + """ + Search OCR-processed media from Flashpoint. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return await self.post("/sources/v2/media", json={"query": query, **kwargs}) + + @log_method_call + async def get_media_object(self, query: str) -> httpx.Response: + """ + Retrieve metadata for a specific media object. + + Args: + query (str): The media_id of the object to retrieve. + """ + return await self.get(f"/sources/v2/media/{query}") + + @log_method_call + async def get_media_image(self, query: str) -> httpx.Response: + """ + Download image asset by storage_uri. + + Args: + query (str): The storage_uri (asset_id) of the image to download. + """ + safe_headers = {"Authorization": f"Bearer {self.api_key}"} + return await self.get("/sources/v1/media/", headers=safe_headers, params={"asset_id": query}) + + @log_method_call + async def search_checks(self, query: str, **kwargs) -> httpx.Response: + """ + Search Flashpoint fraud check datasets asynchronously. + + Args: + query (str): The search string used in the API query. + **kwargs: Additional query logic per the Flashpoint API documentation. + """ + return await self.post("/sources/v2/fraud/checks", json={"query": query, **kwargs}) \ No newline at end of file diff --git a/src/apiary/api_connectors/generic.py b/src/apiary/api_connectors/generic.py new file mode 100644 index 0000000..a1e904d --- /dev/null +++ b/src/apiary/api_connectors/generic.py @@ -0,0 +1,105 @@ +import httpx +from typing import Dict, Any, Optional +from apiary.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call + +@bubble_broker_init_signature() +class GenericConnector(Broker): + """ + A flexible, minimal connector that allows sending arbitrary HTTP requests + using the Broker infrastructure. + """ + + @log_method_call + def request( + self, + method: str, + url: str, + headers: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, Any]] = None, + data: Optional[Any] = None, + json: Optional[Dict[str, Any]] = None, + auth: Optional[Any] = None, + timeout: Optional[int] = None, + **kwargs + ) -> httpx.Response: + """ + Make an arbitrary HTTP request using Broker's request logic. + + Args: + method (str): HTTP method (e.g., GET, POST). + url (str): Fully qualified URL to send the request to. + headers (Optional[Dict[str, str]]): Request headers. + params (Optional[Dict[str, Any]]): Query string parameters. + data (Optional[Any]): Form-encoded or raw data. + json (Optional[Dict[str, Any]]): JSON payload. + auth (Optional[Any]): Authentication (e.g., tuple for basic auth). + timeout (Optional[int]): Request timeout in seconds. + + Returns: + httpx.Response: The response object. + """ + + # Merge headers with base class + merged_headers = self.headers.copy() + if headers: + merged_headers.update(headers) + + return self._make_request( + method=method, + endpoint=url, + headers=merged_headers, + params=params, + json=json, + auth=auth, + retry_kwargs=kwargs.get("retry_kwargs"), + ) + + +@bubble_broker_init_signature() +class AsyncGenericConnector(AsyncBroker): + """ + Async version of GenericConnector using AsyncBroker infrastructure. + """ + + @log_method_call + async def request( + self, + method: str, + url: str, + headers: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, Any]] = None, + data: Optional[Any] = None, + json: Optional[Dict[str, Any]] = None, + auth: Optional[Any] = None, + timeout: Optional[int] = None, + **kwargs + ) -> httpx.Response: + """ + Make an arbitrary HTTP request using AsyncBroker's request logic. + + Args: + method (str): HTTP method (e.g., GET, POST). + url (str): Fully qualified URL to send the request to. + headers (Optional[Dict[str, str]]): Request headers. + params (Optional[Dict[str, Any]]): Query string parameters. + data (Optional[Any]): Form-encoded or raw data. + json (Optional[Dict[str, Any]]): JSON payload. + auth (Optional[Any]): Authentication (e.g., tuple for basic auth). + timeout (Optional[int]): Request timeout in seconds. + + Returns: + httpx.Response: The response object. + """ + merged_headers = self.headers.copy() + if headers: + merged_headers.update(headers) + + return await self._make_request( + method=method, + endpoint=url, + headers=merged_headers, + params=params, + json=json, + auth=auth, + retry_kwargs=kwargs.get("retry_kwargs"), + ) diff --git a/src/apiary/api_connectors/ipqs.py b/src/apiary/api_connectors/ipqs.py new file mode 100644 index 0000000..0d3c324 --- /dev/null +++ b/src/apiary/api_connectors/ipqs.py @@ -0,0 +1,68 @@ +import httpx +from typing import Optional +from urllib.parse import quote +from apiary.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call + +@bubble_broker_init_signature() +class IPQSConnector(Broker): + """ + A connector for the IPQualityScore Malicious URL Scanner API. + + This class provides a typed interface to interact with IPQS's malicious URL + scan endpoint. It handles API key management, header setup, and request routing + through the shared Broker infrastructure. + + Attributes: + api_key (str): The API key used to authenticate with IPQS. + """ + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://ipqualityscore.com/api/json", **kwargs) + + self.api_key = api_key or self.env_config.get("IPQS_API_KEY") + if not self.api_key: + raise ValueError("API key is required for IPQSConnector") + self.headers.update({"Content-Type": "application/json"}) + + @log_method_call + def malicious_url(self, query: str, **kwargs) -> httpx.Response: + """ + Scan a URL using IPQualityScore's Malicious URL Scanner API. + + Args: + query (str): The URL to scan (will be URL-encoded). + **kwargs: Optional parameters like 'strictness' or 'fast' to influence scan behavior. + + Returns: + httpx.Response: the httpx.Response object + """ + encoded_query: str = quote(query, safe="") + return self.post(f"/url/", json={"url": query, "key": self.api_key, **kwargs}) + + +@bubble_broker_init_signature() +class AsyncIPQSConnector(AsyncBroker): + """ + Async version of IPQSConnector using AsyncBroker infrastructure. + """ + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://ipqualityscore.com/api/json", **kwargs) + + self.api_key = api_key or self.env_config.get("IPQS_API_KEY") + if not self.api_key: + raise ValueError("API key is required for AsyncIPQSConnector") + self.headers.update({"Content-Type": "application/json"}) + + @log_method_call + async def malicious_url(self, query: str, **kwargs) -> httpx.Response: + """ + Asynchronously scan a URL using IPQualityScore's Malicious URL Scanner API. + + Args: + query (str): The URL to scan (will be URL-encoded). + **kwargs: Optional parameters like 'strictness' or 'fast' to influence scan behavior. + + Returns: + httpx.Response: the httpx.Response object + """ + encoded_query: str = quote(query, safe="") + return await self.post(f"/url/", json={"url": query, "key": self.api_key, **kwargs}) diff --git a/src/apiary/api_connectors/spycloud.py b/src/apiary/api_connectors/spycloud.py new file mode 100644 index 0000000..e1d2e88 --- /dev/null +++ b/src/apiary/api_connectors/spycloud.py @@ -0,0 +1,207 @@ +import httpx +from typing import Dict, Any, Optional +from apiary.api_connectors.broker import AsyncBroker, Broker, bubble_broker_init_signature, log_method_call + + +@bubble_broker_init_signature() +class SpycloudConnector(Broker): + """ + SpyCloudConnector provides typed methods to interact with various SpyCloud APIs, including: + - SIP Cookie Domains + - ATO Breach Catalog + - ATO Search + - Investigations Search + """ + + def __init__(self, sip_key: Optional[str] = None, ato_key: Optional[str] = None, + inv_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://api.spycloud.io", **kwargs) + self.sip_key = sip_key or self.env_config.get("SPYCLOUD_API_SIP_KEY") + self.ato_key = ato_key or self.env_config.get("SPYCLOUD_API_ATO_KEY") + self.inv_key = inv_key or self.env_config.get("SPYCLOUD_API_INV_KEY") + + @log_method_call + def sip_cookie_domains(self, cookie_domains: str, **kwargs) -> httpx.Response: + """Query SIP cookie domain data.""" + if not self.sip_key: + raise ValueError("SPYCLOUD_API_SIP_KEY is required for this request.") + endpoint = f"/sip-v1/breach/data/cookie-domains/{cookie_domains}" + headers = { + "accept": "application/json", + "x-api-key": self.sip_key + } + return self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) + + @log_method_call + def ato_breach_catalog(self, query: str, **kwargs) -> httpx.Response: + """Query ATO breach catalog.""" + if not self.ato_key: + raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") + endpoint = "/sp-v2/breach/catalog" + headers = { + "accept": "application/json", + "x-api-key": self.ato_key + } + params = {"query": query, **kwargs} + return self._make_request("get", endpoint=endpoint, headers=headers, params=params) + + @log_method_call + def ato_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: + """Search against SpyCloud's ATO breach dataset.""" + if not self.ato_key: + raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") + + base_url = "/sp-v2/breach/data" + endpoints = { + 'domain': 'domains', + 'email': 'emails', + 'ip': 'ips', + 'username': 'usernames', + 'phone-number': 'phone-numbers', + } + + if search_type not in endpoints: + raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') + + endpoint = f"{base_url}/{endpoints[search_type]}/{query}" + headers = { + "accept": "application/json", + "x-api-key": self.ato_key + } + return self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) + + @log_method_call + def investigations_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: + """Search SpyCloud Investigations API by type and query.""" + if not self.inv_key: + raise ValueError("SPYCLOUD_API_INV_KEY is required for this request.") + + base_url = "/investigations-v2/breach/data" + endpoints = { + 'domain': 'domains', + 'email': 'emails', + 'ip': 'ips', + 'infected-machine-id': 'infected-machine-ids', + 'log-id': 'log-ids', + 'password': 'passwords', + 'username': 'usernames', + 'email-username': 'email-usernames', + 'phone-number': 'phone-numbers', + 'social-handle': 'social-handles', + 'bank-number': 'bank-numbers', + 'cc-number': 'cc-numbers', + 'drivers-license': 'drivers-licenses', + 'national-id': 'national-ids', + 'passport-number': 'passport-numbers', + 'ssn': 'social-security-numbers', + } + + if search_type not in endpoints: + raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') + + endpoint = f"{base_url}/{endpoints[search_type]}/{query}" + headers = { + "accept": "application/json", + "x-api-key": self.inv_key + } + return self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) + + +@bubble_broker_init_signature() +class AsyncSpycloudConnector(AsyncBroker): + """ + Async version of SpycloudConnector. + """ + + def __init__(self, sip_key: Optional[str] = None, ato_key: Optional[str] = None, + inv_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://api.spycloud.io", **kwargs) + self.sip_key = sip_key or self.env_config.get("SPYCLOUD_API_SIP_KEY") + self.ato_key = ato_key or self.env_config.get("SPYCLOUD_API_ATO_KEY") + self.inv_key = inv_key or self.env_config.get("SPYCLOUD_API_INV_KEY") + + @log_method_call + async def sip_cookie_domains(self, cookie_domains: str, **kwargs) -> httpx.Response: + """Query SIP cookie domain data (async).""" + if not self.sip_key: + raise ValueError("SPYCLOUD_API_SIP_KEY is required for this request.") + endpoint = f"/sip-v1/breach/data/cookie-domains/{cookie_domains}" + headers = { + "accept": "application/json", + "x-api-key": self.sip_key + } + return await self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) + + @log_method_call + async def ato_breach_catalog(self, query: str, **kwargs) -> httpx.Response: + """Query ATO breach catalog (async).""" + if not self.ato_key: + raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") + endpoint = "/sp-v2/breach/catalog" + headers = { + "accept": "application/json", + "x-api-key": self.ato_key + } + params = {"query": query, **kwargs} + return await self._make_request("get", endpoint=endpoint, headers=headers, params=params) + + @log_method_call + async def ato_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: + """Search against SpyCloud's ATO breach dataset (async).""" + if not self.ato_key: + raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") + + base_url = "/sp-v2/breach/data" + endpoints = { + 'domain': 'domains', + 'email': 'emails', + 'ip': 'ips', + 'username': 'usernames', + 'phone-number': 'phone-numbers', + } + + if search_type not in endpoints: + raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') + + endpoint = f"{base_url}/{endpoints[search_type]}/{query}" + headers = { + "accept": "application/json", + "x-api-key": self.ato_key + } + return await self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) + + @log_method_call + async def investigations_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: + """Search SpyCloud Investigations API by type and query (async).""" + if not self.inv_key: + raise ValueError("SPYCLOUD_API_INV_KEY is required for this request.") + + base_url = "/investigations-v2/breach/data" + endpoints = { + 'domain': 'domains', + 'email': 'emails', + 'ip': 'ips', + 'infected-machine-id': 'infected-machine-ids', + 'log-id': 'log-ids', + 'password': 'passwords', + 'username': 'usernames', + 'email-username': 'email-usernames', + 'phone-number': 'phone-numbers', + 'social-handle': 'social-handles', + 'bank-number': 'bank-numbers', + 'cc-number': 'cc-numbers', + 'drivers-license': 'drivers-licenses', + 'national-id': 'national-ids', + 'passport-number': 'passport-numbers', + 'ssn': 'social-security-numbers', + } + + if search_type not in endpoints: + raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') + + endpoint = f"{base_url}/{endpoints[search_type]}/{query}" + headers = { + "accept": "application/json", + "x-api-key": self.inv_key + } + return await self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) \ No newline at end of file diff --git a/src/apiary/api_connectors/twilio.py b/src/apiary/api_connectors/twilio.py new file mode 100644 index 0000000..b41b586 --- /dev/null +++ b/src/apiary/api_connectors/twilio.py @@ -0,0 +1,114 @@ +from datetime import date, datetime +from typing import Dict, Any, List, Set, Union, Optional +from httpx import BasicAuth, Response +from apiary.api_connectors.broker import AsyncBroker, Broker, bubble_broker_init_signature, log_method_call +from apiary.helpers import validate_date_string + +@bubble_broker_init_signature() +class TwilioConnector(Broker): + """ + Connector for interacting with Twilio Lookup and Usage APIs. + + Supports phone number lookups with data packages and generating account usage reports. + """ + + def __init__( + self, + api_sid: Optional[str] = None, + api_secret: Optional[str] = None, + **kwargs + ): + super().__init__(base_url="https://lookups.twilio.com/v2", **kwargs) + + self.api_sid = api_sid or self.env_config.get("TWILIO_API_SID") + self.api_secret = api_secret or self.env_config.get("TWILIO_API_SECRET") + + if not self.api_sid or not self.api_secret: + raise ValueError("TWILIO_API_SID and TWILIO_API_SECRET are required.") + + self.auth = BasicAuth(self.api_sid, self.api_secret) + + @log_method_call + def lookup_phone(self, phone_number: str, data_packages: Optional[List[str]] = None, **kwargs) -> Response: + """ + Query information about a phone number using Twilio's Lookup API. + + Args: + phone_number (str): The phone number to query. + data_packages (list): Optional data packages to include (e.g. 'caller_name', 'sim_swap'). + + Returns: + httpx.Response: the httpx.Response object + """ + valid_data_packages: Set[str] = { + 'caller_name', 'sim_swap', 'call_forwarding', 'line_status', + 'line_type_intelligence', 'identity_match', 'reassigned_number', + 'sms_pumping_risk', 'phone_number_quality_score', 'pre_fill' + } + + if data_packages: + invalid = set(data_packages) - valid_data_packages + if invalid: + raise ValueError(f"Invalid data packages: {', '.join(invalid)}") + + params: Dict[str, Any] = { + 'Fields': ','.join(data_packages) if data_packages else "", + **kwargs + } + + endpoint = f"/PhoneNumbers/{phone_number}" + return self._make_request("get", endpoint=endpoint, auth=self.auth, params=params) + + +@bubble_broker_init_signature() +class AsyncTwilioConnector(AsyncBroker): + """ + Async connector for interacting with Twilio Lookup API. + """ + + def __init__( + self, + api_sid: Optional[str] = None, + api_secret: Optional[str] = None, + **kwargs + ): + super().__init__(base_url="https://lookups.twilio.com/v2", **kwargs) + + self.api_sid = api_sid or self.env_config.get("TWILIO_API_SID") + self.api_secret = api_secret or self.env_config.get("TWILIO_API_SECRET") + + if not self.api_sid or not self.api_secret: + raise ValueError("TWILIO_API_SID and TWILIO_API_SECRET are required.") + + self.auth = BasicAuth(self.api_sid, self.api_secret) + + @log_method_call + async def lookup_phone(self, phone_number: str, data_packages: Optional[List[str]] = None, **kwargs) -> Response: + """ + Async version of phone number lookup using Twilio Lookup API. + + Args: + phone_number (str): The phone number to query. + data_packages (list): Optional data packages to include (e.g. 'caller_name', 'sim_swap'). + + Returns: + httpx.Response: the httpx.Response object + """ + valid_data_packages: Set[str] = { + 'caller_name', 'sim_swap', 'call_forwarding', 'line_status', + 'line_type_intelligence', 'identity_match', 'reassigned_number', + 'sms_pumping_risk', 'phone_number_quality_score', 'pre_fill' + } + + if data_packages: + invalid = set(data_packages) - valid_data_packages + if invalid: + raise ValueError(f"Invalid data packages: {', '.join(invalid)}") + + params: Dict[str, Any] = { + 'Fields': ','.join(data_packages) if data_packages else "", + **kwargs + } + + endpoint = f"/PhoneNumbers/{phone_number}" + return await self._make_request("get", endpoint=endpoint, auth=self.auth, params=params) \ No newline at end of file diff --git a/src/apiary/api_connectors/urlscan.py b/src/apiary/api_connectors/urlscan.py new file mode 100644 index 0000000..672565e --- /dev/null +++ b/src/apiary/api_connectors/urlscan.py @@ -0,0 +1,148 @@ +import httpx +from typing import Dict, Any, Optional +from apiary.api_connectors.broker import AsyncBroker, Broker, bubble_broker_init_signature, log_method_call + +@bubble_broker_init_signature() +class URLScanConnector(Broker): + """ + A connector for interacting with the urlscan.io API. + + Provides structured methods for submitting scans, querying historical data, + and retrieving detailed scan results and metadata. + + Attributes: + api_key (str): The API key used to authenticate with urlscan.io. + """ + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://urlscan.io", **kwargs) + + self.api_key = api_key or self.env_config.get("URLSCAN_API_KEY") + if not self.api_key: + raise ValueError("API key is required for URLScanConnector") + self.headers.update({ + "accept": "application/json", + "API-Key": self.api_key + }) + + @log_method_call + def search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Search for archived scans matching a given query. + + Args: + query (str): The search term or filter string. + **kwargs: Additional query parameters for filtering results. + + Returns: + httpx.Response: the httpx.Response object + """ + params = {"q": query, **kwargs} + return self.get("/api/v1/search/", params=params) + + @log_method_call + def scan(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Submit a URL to be scanned by urlscan.io. + + Args: + query (str): The URL to scan. + **kwargs: Additional scan options like tags, visibility, or referer. + + Returns: + httpx.Response: the httpx.Response object + """ + payload = {"url": query, **kwargs} + return self.post("/api/v1/scan", json=payload) + + @log_method_call + def results(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Retrieve detailed scan results by UUID. + + Args: + query (str): The UUID of the scan. + + Returns: + httpx.Response: the httpx.Response object + """ + return self.get(f"/api/v1/result/{query}", params=kwargs) + + @log_method_call + def get_dom(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Retrieve the DOM snapshot for a given scan UUID. + + Args: + query (str): The UUID of the scan. + + Returns: + httpx.Response: the httpx.Response object + """ + return self.get(f"/dom/{query}", params=kwargs) + + @log_method_call + def structure_search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Search for scans structurally similar to a given UUID. + + Args: + query (str): The UUID of the original scan. + + Returns: + httpx.Response: the httpx.Response object + """ + return self.get(f"/api/v1/pro/result/{query}/similar", params=kwargs) + +class AsyncURLScanConnector(AsyncBroker): + """ + An async connector for interacting with the urlscan.io API. + """ + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(base_url="https://urlscan.io", **kwargs) + + self.api_key = api_key or self.env_config.get("URLSCAN_API_KEY") + if not self.api_key: + raise ValueError("API key is required for AsyncURLScanConnector") + self.headers.update({ + "accept": "application/json", + "API-Key": self.api_key + }) + + @log_method_call + async def search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Async search for archived scans matching a given query. + """ + params = {"q": query, **kwargs} + return await self.get("/api/v1/search/", params=params) + + @log_method_call + async def scan(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Async submit a URL to be scanned by urlscan.io. + """ + payload = {"url": query, **kwargs} + return await self.post("/api/v1/scan", json=payload) + + @log_method_call + async def results(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Async retrieve detailed scan results by UUID. + """ + return await self.get(f"/api/v1/result/{query}", params=kwargs) + + @log_method_call + async def get_dom(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Async retrieve the DOM snapshot for a given scan UUID. + """ + return await self.get(f"/dom/{query}", params=kwargs) + + @log_method_call + async def structure_search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: + """ + Async search for scans structurally similar to a given UUID. + """ + return await self.get(f"/api/v1/pro/result/{query}/similar", params=kwargs) diff --git a/src/apiary/dbms_connectors/__init__.py b/src/apiary/dbms_connectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apiary/dbms_connectors/elasticsearch.py b/src/apiary/dbms_connectors/elasticsearch.py new file mode 100644 index 0000000..3abdf12 --- /dev/null +++ b/src/apiary/dbms_connectors/elasticsearch.py @@ -0,0 +1,143 @@ +from elasticsearch import Elasticsearch, helpers +from typing import List, Dict, Generator, Any, Optional, Union + + +try: + from apiary.helpers import setup_logger + _default_logger = setup_logger(name="elasticsearch") +except ImportError: + import logging + _default_logger = logging.getLogger("elasticsearch") + if not _default_logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s') + handler.setFormatter(formatter) + _default_logger.addHandler(handler) + _default_logger.setLevel(logging.INFO) + + +class ElasticsearchConnector: + """ + A connector class for interacting with Elasticsearch. + + This class provides methods to perform paginated search queries using the scroll API + and to execute bulk insert operations. It includes integrated logging support for observability. + """ + def __init__( + self, + hosts: List[str], + username: Optional[str] = None, + password: Optional[str] = None, + logger: Optional[Any] = None + ): + """ + Initialize the Elasticsearch client. + + Args: + hosts (List[str]): List of Elasticsearch host URLs. + username (Optional[str]): Username for basic authentication. Defaults to None. + password (Optional[str]): Password for basic authentication. Defaults to None. + logger (Optional[Any]): Optional logger instance. If not provided, a default logger is used. + """ + self.client = Elasticsearch(hosts, basic_auth=(username, password)) + self.logger = logger if logger is not None else _default_logger + + def _log(self, msg: str, level: str = "info"): + """ + Internal helper to log messages using the provided or default logger. + + Args: + msg (str): The message to log. + level (str): The logging level as a string (e.g., 'info', 'error'). Defaults to 'info'. + """ + if self.logger: + log_method = getattr(self.logger, level, self.logger.info) + log_method(msg) + + def query( + self, + index: str, + query: Union[str, Dict], + size: int = 1000 + ) -> Generator[Dict[str, Any], None, None]: + """ + Execute a paginated search query using the Elasticsearch scroll API. + + This method handles retrieval of large result sets by paging through results + using a scroll context. + + Args: + index (str): The name of the index to search. + query (Union[str, Dict]): A Lucene query string or Elasticsearch DSL query body. + size (int): Number of results to retrieve per batch. Defaults to 1000. + + Yields: + Generator[Dict[str, Any], None, None]: A generator that yields each search hit as a dictionary. + + Note: + This method returns a generator. If you want to collect all results, + you can wrap the result in `list()`, but beware of memory usage if the + result set is large. Prefer streaming and processing results incrementally. + """ + + self._log(f"Executing query on index '{index}' with batch size {size}", "info") + + if isinstance(query, str): + query = { + "query": { + "query_string": { + "query": query + } + } + } + + page = self.client.search(index=index, body=query, scroll="2m", size=size) + sid = page["_scroll_id"] + hits = page["hits"]["hits"] + yield from hits + + while hits: + page = self.client.scroll(scroll_id=sid, scroll="2m") + sid = page["_scroll_id"] + hits = page["hits"]["hits"] + if not hits: + break + yield from hits + self.client.clear_scroll(scroll_id=sid) + self._log(f"Completed scrolling query on index '{index}'", "info") + + def bulk_insert( + self, + index: str, + data: List[Dict], + id_key: str = "_id" + ): + """ + Perform a bulk insert operation into the specified Elasticsearch index. + + This method sends batches of documents for indexing in a single API call. + Each document can optionally specify an ID via the `id_key`. + + Args: + index (str): The name of the index to insert documents into. + data (List[Dict]): A list of documents to insert. + id_key (str): The key in each document to use as the document ID. Defaults to "_id". + + Returns: + Tuple[int, List[Dict]]: A tuple containing the number of successfully processed actions + and a list of any errors encountered during insertion. + """ + self._log(f"Inserting {len(data)} documents into index '{index}'", "info") + actions = [ + { + "_index": index, + "_id": doc.get(id_key), + "_source": doc + } for doc in data + ] + success, errors = helpers.bulk(self.client, actions) + if errors: + self._log(f"Bulk insert encountered errors: {errors}", "error") + else: + self._log("Bulk insert completed successfully", "info") + return success, errors diff --git a/src/apiary/dbms_connectors/mongo.py b/src/apiary/dbms_connectors/mongo.py new file mode 100644 index 0000000..8af2e86 --- /dev/null +++ b/src/apiary/dbms_connectors/mongo.py @@ -0,0 +1,390 @@ +from pymongo import MongoClient, UpdateOne +from pymongo.errors import ( + OperationFailure, + ServerSelectionTimeoutError, + AutoReconnect, + ConnectionFailure, +) +from tenacity import Retrying, stop_after_attempt, wait_fixed, retry_if_exception_type +from typing import List, Dict, Any, Optional, Generator, Type, Union +from types import TracebackType +from apiary.helpers import setup_logger + + +_DEFAULT_LOGGER = object() + + +class MongoConnector: + """ + A connector class for interacting with MongoDB. + + Provides methods for finding documents with paging and performing batched + insert and upsert operations, as well as convenience helpers for + `distinct`, `delete`, and `delete_many`. + + Supports explicit lifecycle management via `close()` and can be used as a + context manager (`with MongoConnector(...) as conn:`). On initialization, + the connector pings the server to validate connectivity/authentication with + a simple retry policy. + Logs actions if a logger is provided. + """ + def __init__( + self, + uri: str, + username: Optional[str] = None, + password: Optional[str] = None, + auth_source: str = "admin", + timeout: int = 10, + auth_mechanism: Optional[str] = "DEFAULT", + ssl: Optional[bool] = True, + logger: Optional[Any] = _DEFAULT_LOGGER, + auth_retry_attempts: int = 3, + auth_retry_wait: float = 1.0, + ): + """ + Initialize the MongoDB client. + + Args: + uri (str): The MongoDB connection URI. + username (Optional[str]): Username for authentication. Defaults to None. + password (Optional[str]): Password for authentication. Defaults to None. + auth_source (str): The authentication database. Defaults to "admin". + timeout (int): Server selection timeout in seconds. Defaults to 10. + auth_mechanism (Optional[str]): Authentication mechanism for MongoDB (e.g., "SCRAM-SHA-1"). + ssl (Optional[bool]): Whether to use SSL for the connection. + logger (Optional[Any]): Logger instance for logging actions. Defaults to a module logger when omitted; pass None to disable logging. + auth_retry_attempts (int): Number of attempts for initial auth ping. Defaults to 3. + auth_retry_wait (float): Seconds to wait between auth attempts. Defaults to 1.0. + """ + # Initialize MongoClient with authSource, authMechanism, and ssl options + self.client = MongoClient( + uri, + username=username, + password=password, + authSource=auth_source, + authMechanism=auth_mechanism, + ssl=ssl, + serverSelectionTimeoutMS=timeout * 1000 + ) + self.logger = setup_logger(__name__) if logger is _DEFAULT_LOGGER else logger + self.auth_retry_attempts = auth_retry_attempts + self.auth_retry_wait = auth_retry_wait + self._log( + f"Initialized MongoClient with authSource={auth_source}, " + f"authMechanism={auth_mechanism}, ssl={ssl}" + ) + # Force an initial ping to trigger auth/handshake; retry to handle + # clusters that intermittently fail the first attempt. + self._ping_with_retry() + + def close(self) -> None: + """Close the underlying MongoClient connection.""" + self._log("Closing MongoClient connection", level="debug") + try: + self.client.close() + except Exception as exc: + # Closing should be best-effort; log and continue + self._log(f"Error during MongoClient.close(): {exc}", level="warning") + + # Context manager support + def __enter__(self) -> "MongoConnector": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + self.close() + + def _log(self, msg: str, level: str = "info"): + """ + Internal helper method for logging. + + Args: + msg (str): The message to log. + level (str): Logging level as string (e.g., "info", "debug"). Defaults to "info". + """ + if self.logger: + log_method = getattr(self.logger, level, self.logger.info) + log_method(msg) + + def _ping_with_retry(self) -> None: + """Ping the server to validate connection/auth, with retry.""" + for attempt in Retrying( + stop=stop_after_attempt(self.auth_retry_attempts), + wait=wait_fixed(self.auth_retry_wait), + reraise=True, + retry=retry_if_exception_type( + (OperationFailure, ServerSelectionTimeoutError, AutoReconnect, ConnectionFailure) + ), + ): + with attempt: + self._log("Pinging MongoDB to verify connection/auth...", level="debug") + # 'ping' triggers handshake and, when needed, authentication + self.client.admin.command("ping") + + def find( + self, + db_name: str, + collection: str, + filter: Dict, + projection: Optional[Dict] = None, + batch_size: int = 1000 + ) -> Generator[Dict[str, Any], None, None]: + """ + Find documents in a MongoDB collection with optional projection and paging. + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + filter (Dict): MongoDB filter document. + projection (Optional[Dict]): Fields to include or exclude. Defaults to None. + batch_size (int): Number of documents per batch. Defaults to 1000. + + Yields: + Dict[str, Any]: Each document as a dictionary. + + Logs: + Logs the find operation with filter details. + """ + self._log(f"Executing Mongo find on {db_name}.{collection}") + col = self.client[db_name][collection] + cursor = col.find(filter, projection).batch_size(batch_size) + for doc in cursor: + yield doc + + def aggregate( + self, + db_name: str, + collection: str, + pipeline: List[Dict[str, Any]], + batch_size: Optional[int] = None, + **kwargs: Any, + ) -> Generator[Dict[str, Any], None, None]: + """ + Run an aggregation pipeline on a collection and stream results. + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + pipeline (List[Dict[str, Any]]): Aggregation pipeline stages. + batch_size (Optional[int]): If provided, set cursor batch size. + **kwargs: Additional options forwarded to `Collection.aggregate` (e.g., + allowDiskUse, collation, maxTimeMS, comment). + + Yields: + Dict[str, Any]: Each document from the aggregation result. + """ + self._log( + f"Executing Mongo aggregate on {db_name}.{collection}" + ) + col = self.client[db_name][collection] + cursor = col.aggregate(pipeline, **kwargs) + if batch_size is not None: + cursor = cursor.batch_size(batch_size) + for doc in cursor: + yield doc + + def query( + self, + db_name: str, + collection: str, + query: Dict, + projection: Optional[Dict] = None, + batch_size: int = 1000 + ) -> Generator[Dict[str, Any], None, None]: + """ + Deprecated: use `find` instead. + + Backwards-compatible wrapper that forwards to `find`. + """ + self._log( + "MongoConnector.query is deprecated; use MongoConnector.find instead", + level="warning", + ) + return self.find( + db_name=db_name, + collection=collection, + filter=query, + projection=projection, + batch_size=batch_size, + ) + + def insert_many( + self, + db_name: str, + collection: str, + data: List[Dict], + ordered: bool = False, + batch_size: int = 1000, + ): + """ + Insert multiple documents into a collection (batched). + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + data (List[Dict]): Documents to insert. + ordered (bool): Whether operations should be ordered. Defaults to False. + batch_size (int): Number of documents per batch. Defaults to 1000. + + Returns: + List: List of InsertManyResult objects for each batch. + + Note: + PyMongo batches writes internally; manual batching here is useful + for memory control, error isolation, and progress logging. + """ + self._log( + f"insert_many: inserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}" + ) + col = self.client[db_name][collection] + results = [] + for i in range(0, len(data), batch_size): + batch = data[i:i + batch_size] + result = col.insert_many(batch, ordered=ordered) + results.append(result) + return results + + def upsert_many( + self, + db_name: str, + collection: str, + data: List[Dict], + unique_key: Optional[Union[str, List[str]]], + ordered: bool = False, + batch_size: int = 1000, + ): + """ + Upsert multiple documents into a collection using a unique key or keys (batched). + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + data (List[Dict]): Documents to upsert. + unique_key (Optional[Union[str, List[str]]]): Field name or list of field names to use for upsert filtering. + If a list, the filter is built as a compound key using all specified fields. + ordered (bool): Whether operations should be ordered. Defaults to False. + batch_size (int): Number of documents per batch. Defaults to 1000. + + Returns: + List: List of BulkWriteResult objects for each batch. + + Details: + Uses `bulk_write` with `UpdateOne(filter, {"$set": doc}, upsert=True)` + to merge fields from each document. The `unique_key` or all keys in the list + must exist in each document. + """ + if not unique_key: + raise ValueError("unique_key must be provided for upsert_many") + self._log( + f"upsert_many: upserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}, unique_key={unique_key}" + ) + col = self.client[db_name][collection] + results = [] + for i in range(0, len(data), batch_size): + batch = data[i:i + batch_size] + operations = [] + for doc in batch: + if isinstance(unique_key, str): + if unique_key in doc: + filter_doc = {unique_key: doc[unique_key]} + else: + continue + elif isinstance(unique_key, list): + if all(k in doc for k in unique_key): + filter_doc = {k: doc[k] for k in unique_key} + else: + continue + else: + raise ValueError("unique_key must be either a string or a list of strings") + operations.append(UpdateOne(filter_doc, {"$set": doc}, upsert=True)) + if operations: + result = col.bulk_write(operations, ordered=ordered) + results.append(result) + return results + + def distinct( + self, + db_name: str, + collection: str, + key: str, + filter: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> List[Any]: + """ + Return a list of distinct values for `key` across documents. + + This is a thin wrapper around PyMongo's `Collection.distinct` and accepts + any additional keyword arguments supported by PyMongo (e.g., collation, + maxTimeMS, comment). + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + key (str): Field name for which to return distinct values. + filter (Optional[Dict[str, Any]]): Optional query filter to limit the scope. + **kwargs: Additional options forwarded to `Collection.distinct`. + + Returns: + List[Any]: Distinct values for the specified key. + """ + self._log( + f"Executing Mongo distinct on {db_name}.{collection} for key='{key}'" + ) + col = self.client[db_name][collection] + return col.distinct(key, filter, **kwargs) + + def delete( + self, + db_name: str, + collection: str, + filter: Dict[str, Any], + **kwargs: Any, + ): + """ + Delete a single document matching the filter (wrapper over delete_one). + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + filter (Dict[str, Any]): Query filter selecting the document to delete. + **kwargs: Additional options forwarded to `Collection.delete_one` (e.g., collation, comment). + + Returns: + DeleteResult: The result of the delete operation. + """ + self._log( + f"Deleting one from {db_name}.{collection}", + level="info", + ) + col = self.client[db_name][collection] + return col.delete_one(filter, **kwargs) + + def delete_many( + self, + db_name: str, + collection: str, + filter: Dict[str, Any], + **kwargs: Any, + ): + """ + Delete all documents matching the filter (wrapper over delete_many). + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + filter (Dict[str, Any]): Query filter selecting documents to delete. + **kwargs: Additional options forwarded to `Collection.delete_many` (e.g., collation, comment). + + Returns: + DeleteResult: The result of the delete operation. + """ + self._log( + f"Deleting many from {db_name}.{collection}", + level="info", + ) + col = self.client[db_name][collection] + return col.delete_many(filter, **kwargs) diff --git a/src/apiary/dbms_connectors/mongo_async.py b/src/apiary/dbms_connectors/mongo_async.py new file mode 100644 index 0000000..ff9240c --- /dev/null +++ b/src/apiary/dbms_connectors/mongo_async.py @@ -0,0 +1,323 @@ +from __future__ import annotations +from typing import Any, Dict, List, Optional, AsyncIterator, Type, Union +from types import TracebackType +import inspect +from pymongo import UpdateOne +from pymongo.errors import ( + OperationFailure, + ServerSelectionTimeoutError, + AutoReconnect, + ConnectionFailure, +) +from tenacity import AsyncRetrying, stop_after_attempt, wait_fixed, retry_if_exception_type +from apiary.helpers import setup_logger + + +_DEFAULT_LOGGER = object() + + +class AsyncMongoConnector: + """ + An asyncio connector for interacting with MongoDB using PyMongo's async client. + + Mirrors the synchronous MongoConnector API with async methods and context + management (`async with AsyncMongoConnector(...) as conn:`). On entry, the + connector pings the server with a simple retry policy to validate the + connection and trigger authentication. + + Args: + uri (str): The MongoDB connection URI. + username (Optional[str]): Username for authentication. Defaults to None. + password (Optional[str]): Password for authentication. Defaults to None. + auth_source (str): The authentication database. Defaults to "admin". + timeout (int): Server selection timeout in seconds. Defaults to 10. + auth_mechanism (Optional[str]): Authentication mechanism for MongoDB (e.g., "SCRAM-SHA-1"). + ssl (Optional[bool]): Whether to use SSL for the connection. + logger (Optional[Any]): Logger instance for logging actions. Defaults to a module logger when omitted; pass None to disable logging. + auth_retry_attempts (int): Number of attempts for initial auth ping. Defaults to 3. + auth_retry_wait (float): Seconds to wait between auth attempts. Defaults to 1.0. + """ + + def __init__( + self, + uri: str, + username: Optional[str] = None, + password: Optional[str] = None, + auth_source: str = "admin", + timeout: int = 10, + auth_mechanism: Optional[str] = "DEFAULT", + ssl: Optional[bool] = True, + logger: Optional[Any] = _DEFAULT_LOGGER, + auth_retry_attempts: int = 3, + auth_retry_wait: float = 1.0, + ) -> None: + # Import the asyncio client lazily to provide a clear error if unavailable + AsyncMongoClient = None # type: ignore + import_error: Optional[Exception] = None + try: + from pymongo.asynchronous.mongo_client import AsyncMongoClient # type: ignore + except Exception as e1: # pragma: no cover - fallback path + import_error = e1 + try: + # Some versions may expose it at package level + from pymongo.asynchronous import AsyncMongoClient as _AltAsyncClient # type: ignore + AsyncMongoClient = _AltAsyncClient # type: ignore + except Exception as e2: # pragma: no cover - fallback path + import_error = e2 + try: + # Older preview namespace + from pymongo.asyncio import MongoClient as _AsyncioClient # type: ignore + AsyncMongoClient = _AsyncioClient # type: ignore + except Exception as e3: # pragma: no cover - final fallback + import_error = e3 + + if AsyncMongoClient is None: # type: ignore + raise ImportError( + "PyMongo async client is not available. Ensure a recent pymongo version is installed." + ) from import_error + + self.client = AsyncMongoClient( # type: ignore[misc] + uri, + username=username, + password=password, + authSource=auth_source, + authMechanism=auth_mechanism, + ssl=ssl, + serverSelectionTimeoutMS=timeout * 1000, + ) + self.logger = setup_logger(__name__) if logger is _DEFAULT_LOGGER else logger + self._log( + f"Initialized AsyncMongoClient with authSource={auth_source}, " + f"authMechanism={auth_mechanism}, ssl={ssl}" + ) + self.auth_retry_attempts = auth_retry_attempts + self.auth_retry_wait = auth_retry_wait + + # Async context manager + async def __aenter__(self) -> "AsyncMongoConnector": + await self._ping_with_retry() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + await self.close() + + def _log(self, msg: str, level: str = "info") -> None: + if self.logger: + log_method = getattr(self.logger, level, self.logger.info) + log_method(msg) + + async def _ping_with_retry(self) -> None: + """Async ping to validate connection/auth, with retry.""" + async for attempt in AsyncRetrying( + stop=stop_after_attempt(self.auth_retry_attempts), + wait=wait_fixed(self.auth_retry_wait), + reraise=True, + retry=retry_if_exception_type( + (OperationFailure, ServerSelectionTimeoutError, AutoReconnect, ConnectionFailure) + ), + ): + with attempt: + self._log("Pinging MongoDB (async) to verify connection/auth...", level="debug") + await self.client.admin.command("ping") + + # Operations + async def find( + self, + db_name: str, + collection: str, + filter: Dict[str, Any], + projection: Optional[Dict[str, Any]] = None, + batch_size: int = 1000, + ) -> AsyncIterator[Dict[str, Any]]: + """ + Async find documents with optional projection and paging. + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + filter (Dict[str, Any]): MongoDB filter document. + projection (Optional[Dict[str, Any]]): Fields to include/exclude. + batch_size (int): Number of documents per batch. + + Yields: + Each document as a dictionary. + """ + self._log( + f"Executing async Mongo find on {db_name}.{collection}" + ) + col = self.client[db_name][collection] + cursor = col.find(filter, projection).batch_size(batch_size) + async for doc in cursor: + yield doc + + async def aggregate( + self, + db_name: str, + collection: str, + pipeline: List[Dict[str, Any]], + batch_size: Optional[int] = None, + **kwargs: Any, + ) -> AsyncIterator[Dict[str, Any]]: + """ + Async aggregation pipeline execution yielding documents. + + Args: + db_name (str): Name of the database. + collection (str): Name of the collection. + pipeline (List[Dict[str, Any]]): Aggregation pipeline stages. + batch_size (Optional[int]): If provided, set cursor batch size. + **kwargs: Additional options for `Collection.aggregate` (e.g., allowDiskUse). + + Yields: + Each document from the aggregation result. + + Note: + This returns an async iterator. Use `async for` to consume it. + Do not `await` the return value directly. + """ + self._log( + f"Executing async Mongo aggregate on {db_name}.{collection}" + ) + col = self.client[db_name][collection] + # Some PyMongo async versions require awaiting aggregate() to get a cursor + result = col.aggregate(pipeline, **kwargs) + cursor = await result if inspect.isawaitable(result) else result + if batch_size is not None: + cursor = cursor.batch_size(batch_size) + async for doc in cursor: + yield doc + + async def insert_many( + self, + db_name: str, + collection: str, + data: List[Dict[str, Any]], + ordered: bool = False, + batch_size: int = 1000, + ) -> List[Any]: + """ + Async insert multiple documents into a collection (batched). + + Note: + PyMongo batches writes internally as well; manual batching is + primarily for progress logging, memory control, and error isolation. + """ + self._log( + f"async insert_many: inserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}" + ) + col = self.client[db_name][collection] + results: List[Any] = [] + for i in range(0, len(data), batch_size): + batch = data[i : i + batch_size] + result = await col.insert_many(batch, ordered=ordered) + results.append(result) + return results + + async def upsert_many( + self, + db_name: str, + collection: str, + data: List[Dict[str, Any]], + unique_key: Optional[Union[str, List[str]]], + ordered: bool = False, + batch_size: int = 1000, + ) -> List[Any]: + """ + Async upsert multiple documents using a unique key (batched). + + Uses `bulk_write` with `UpdateOne(..., upsert=True)` and `$set` to + merge fields from each document. + + Args: + unique_key (Union[str, List[str]]): A string key or list of strings representing + the unique key(s) used to build the filter for upsert operations. + If a list is provided, it is treated as a compound unique key. + """ + if not unique_key: + raise ValueError("unique_key must be provided for upsert_many") + if not (isinstance(unique_key, str) or (isinstance(unique_key, list) and all(isinstance(k, str) for k in unique_key))): + raise ValueError("unique_key must be a string or a list of strings") + self._log( + f"async upsert_many: upserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}, unique_key={unique_key}" + ) + col = self.client[db_name][collection] + results: List[Any] = [] + for i in range(0, len(data), batch_size): + batch = data[i : i + batch_size] + operations = [] + for doc in batch: + if isinstance(unique_key, str): + if unique_key in doc: + filter_doc = {unique_key: doc[unique_key]} + else: + continue + else: + # unique_key is a list of strings + filter_doc = {k: doc[k] for k in unique_key if k in doc} + if len(filter_doc) != len(unique_key): + continue + operations.append(UpdateOne(filter_doc, {"$set": doc}, upsert=True)) + if operations: + result = await col.bulk_write(operations, ordered=ordered) + results.append(result) + return results + + async def distinct( + self, + db_name: str, + collection: str, + key: str, + filter: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> List[Any]: + """Async distinct values for a key, with optional filter.""" + self._log( + f"Executing async Mongo distinct on {db_name}.{collection} for key='{key}'" + ) + col = self.client[db_name][collection] + return await col.distinct(key, filter, **kwargs) + + async def delete( + self, + db_name: str, + collection: str, + filter: Dict[str, Any], + **kwargs: Any, + ) -> Any: + """Async delete a single document matching the filter.""" + self._log( + f"Async deleting one from {db_name}.{collection}", + level="info", + ) + col = self.client[db_name][collection] + return await col.delete_one(filter, **kwargs) + + async def delete_many( + self, + db_name: str, + collection: str, + filter: Dict[str, Any], + **kwargs: Any, + ) -> Any: + """Async delete all documents matching the filter.""" + self._log( + f"Async deleting many from {db_name}.{collection}", + level="info", + ) + col = self.client[db_name][collection] + return await col.delete_many(filter, **kwargs) + + async def close(self) -> None: + """Close the underlying async client.""" + self._log("Closing AsyncMongoClient connection", level="debug") + try: + result = self.client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # pragma: no cover - best-effort close + self._log(f"Error during AsyncMongoClient.close(): {exc}", level="warning") diff --git a/src/apiary/dbms_connectors/odbc.py b/src/apiary/dbms_connectors/odbc.py new file mode 100644 index 0000000..ac5519c --- /dev/null +++ b/src/apiary/dbms_connectors/odbc.py @@ -0,0 +1,110 @@ +from importlib import import_module +from typing import Any, Dict, Generator, List +from apiary.helpers import setup_logger + +_PYODBC_MODULE = None + + +def _get_pyodbc(): + """ + Lazily import pyodbc so the package stays importable without the optional dep. + """ + global _PYODBC_MODULE + if _PYODBC_MODULE is None: + try: + _PYODBC_MODULE = import_module("pyodbc") + except ImportError as exc: + raise ImportError( + "pyodbc is not installed. Install apiary[odbc] or add pyodbc to your deps." + ) from exc + return _PYODBC_MODULE + + +class ODBCConnector: + """ + A connector class for interacting with ODBC-compatible databases. + + Provides methods for querying and bulk inserts. + Supports use as a context manager for automatic connection cleanup. + """ + + def __init__(self, conn_str: str, logger: Any = None): + """ + Initialize the ODBC connection. + + Args: + conn_str (str): The ODBC connection string. + logger (Any, optional): Logger instance for logging. Defaults to None. + """ + pyodbc = _get_pyodbc() + self.conn = pyodbc.connect(conn_str) + self.logger = logger or setup_logger(__name__) + self._log("ODBC connection established") + + def _log(self, msg: str, level: str = "info"): + if self.logger: + log_method = getattr(self.logger, level, self.logger.info) + log_method(msg) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + """Close the ODBC connection.""" + if self.conn: + self.conn.close() + self._log("ODBC connection closed") + + def query(self, base_query: str) -> Generator[Dict[str, Any], None, None]: + """ + Execute a query against an ODBC database and yield each row as a dictionary. + + Args: + base_query (str): The SQL query to execute. + + Yields: + Generator[Dict[str, Any], None, None]: A generator that yields each search hit as a dictionary. + + Logs: + Execution of the query. + + Note: + This method returns a generator. If you want to collect all results, + you can wrap the result in `list()`, but beware of memory usage if the + result set is large. + """ + self._log(f"Executing ODBC query") + cursor = self.conn.cursor() + cursor.execute(base_query) + columns = [col[0] for col in cursor.description] + for row in cursor.fetchall(): + yield dict(zip(columns, row)) + + def bulk_insert(self, table: str, data: List[Dict[str, Any]]): + """ + Perform a bulk insert into an ODBC database table. + + Args: + table (str): Name of the table to insert into. + data (List[Dict[str, Any]]): List of rows to insert. + + Returns: + None + + Logs: + Number of rows inserted and target table. + """ + if not data: + return + self._log(f"Inserting {len(data)} rows into table {table}") + columns = list(data[0].keys()) + placeholders = ", ".join(["?"] * len(columns)) + insert_query = f"INSERT INTO {table} ({', '.join(columns)}) VALUES ({placeholders})" + values = [tuple(row[col] for col in columns) for row in data] + cursor = self.conn.cursor() + cursor.fast_executemany = True + cursor.executemany(insert_query, values) + self.conn.commit() diff --git a/src/apiary/dbms_connectors/splunk.py b/src/apiary/dbms_connectors/splunk.py new file mode 100644 index 0000000..efd05b7 --- /dev/null +++ b/src/apiary/dbms_connectors/splunk.py @@ -0,0 +1,131 @@ +import httpx +from typing import Generator, Dict, Any, Optional +from apiary.helpers import setup_logger + + +class SplunkConnector: + """ + A connector class for interacting with Splunk via its REST API. + + Provides methods for submitting search jobs and streaming paginated results. + """ + def __init__( + self, + host: str, + port: int, + username: Optional[str] = None, + password: Optional[str] = None, + scheme: str = "https", + verify: bool = True, + timeout: int = 30, + logger: Optional[Any] = None + ): + """ + Initialize the SplunkConnector with connection details. + + Args: + host (str): The Splunk server host. + port (int): The Splunk management port. + username (Optional[str]): Username for authentication. Defaults to None. + password (Optional[str]): Password for authentication. Defaults to None. + scheme (str): HTTP or HTTPS. Defaults to "https". + verify (bool): Whether to verify SSL certificates. Defaults to True. + timeout (int): Request timeout in seconds. Defaults to 30. + logger (Optional[Any]): Logger instance. If provided, actions will be logged. + """ + self.base_url = f"{scheme}://{host}:{port}" + self.auth = (username, password) + self.verify = verify + self.timeout = timeout + self.logger = logger or setup_logger(__name__) + + # Suppress InsecureRequestWarnings if the user sets verify=False + if not self.verify: + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + def _log(self, msg: str, level: str = "info"): + if self.logger: + log_method = getattr(self.logger, level, self.logger.info) + log_method(msg) + + def query( + self, + search: str, + count: int = 1000, + earliest_time: Optional[str] = None, + latest_time: Optional[str] = None + ) -> Generator[Dict[str, Any], None, None]: + """ + Submit a search job to Splunk and stream results as dictionaries. + + Args: + search (str): The search query string. + count (int): Number of results per batch. Defaults to 1000. + earliest_time (Optional[str]): Earliest time for the search. Defaults to None. + latest_time (Optional[str]): Latest time for the search. Defaults to None. + + Yields: + Dict[str, Any]: Each search result as a dictionary. + + Logs actions if logger is enabled. + """ + # 1️⃣ Create job + self._log(f"Submitting search job: {search}") + data = { + "search": search, + "output_mode": "json", + "count": count + } + if earliest_time: + data["earliest_time"] = earliest_time + if latest_time: + data["latest_time"] = latest_time + + create_resp = httpx.post( + f"{self.base_url}/services/search/jobs", + auth=self.auth, + data=data, + verify=self.verify, + timeout=self.timeout + ) + create_resp.raise_for_status() + sid = create_resp.json()["sid"] + + # 2️⃣ Poll until ready + while True: + self._log(f"Polling job {sid} status...") + status_resp = httpx.get( + f"{self.base_url}/services/search/jobs/{sid}", + auth=self.auth, + params={"output_mode": "json"}, + verify=self.verify, + timeout=self.timeout + ) + status_resp.raise_for_status() + content = status_resp.json() + if content["entry"][0]["content"]["isDone"]: + break + + # 3️⃣ Fetch results + offset = 0 + while True: + self._log(f"Fetching results batch starting at offset {offset}") + results_resp = httpx.get( + f"{self.base_url}/services/search/jobs/{sid}/results", + auth=self.auth, + params={ + "output_mode": "json", + "count": count, + "offset": offset + }, + verify=self.verify, + timeout=self.timeout + ) + results_resp.raise_for_status() + results = results_resp.json().get("results", []) + if not results: + break + for row in results: + yield row + offset += len(results) From c2bdc500d70188a72e33d7818da404aad137d963 Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:38:19 -0500 Subject: [PATCH 3/9] update imports --- src/apiary/helpers.py | 102 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/apiary/helpers.py diff --git a/src/apiary/helpers.py b/src/apiary/helpers.py new file mode 100644 index 0000000..3f014ef --- /dev/null +++ b/src/apiary/helpers.py @@ -0,0 +1,102 @@ +from datetime import datetime +from dotenv import dotenv_values, find_dotenv +import logging +import os +import sys +from typing import Dict, Set, List, Any, Optional + + +def check_required_env_vars(config: Dict[str, str], required_vars: List[str]) -> None: + """Ensure that the env variables required for a function are present either in \ + the .env file, or in the system's environment variables. + + Args: + config (Dict[str, str]): the env_config variable that contains values from the .env file + required_vars (List[str]): the env variables required for a function to successfully function + """ + + dotenv_missing_vars: Set[str] = set(required_vars) - set(config.keys()) + osenv_missing_vars: Set[str] = set(required_vars) - set(os.environ) + missing_vars = dotenv_missing_vars | osenv_missing_vars + + if dotenv_missing_vars and osenv_missing_vars: + print(f'[!] Error: missing required environment variables: {", ".join(missing_vars)}. ' + 'Please ensure these are present either in your .env file, or in the ' + 'system\'s environment variables.', file=sys.stderr) + sys.exit(1) + + +def combine_env_configs() -> Dict[str, Any]: + """Find a .env file if it exists, and combine it with system environment + variables to form a "combined_config" dictionary of environment variables + + Returns: + Dict: a dictionary containing the output of a .env file (if found), and + system environment variables + """ + + env_config: Dict[str, Any] = dict(dotenv_values(find_dotenv())) + + combined_config: Dict[str, Any] = {**env_config, **dict(os.environ)} + + return combined_config + + +def validate_date_string(date_str: str) -> bool: + """Validates that a date string is, well, a valid date string + + Args: + date_str (str): a string in "YYYY-MM-DD" format + + Returns: + bool: True or False as valid or not + """ + try: + datetime.strptime(date_str, "%Y-%m-%d") + return True + except ValueError: + return False + + +def setup_logger( + name: str = __name__, + level: int = logging.INFO, + log_file: Optional[str] = None, + use_stdout: bool = True +) -> logging.Logger: + """ + Configures and returns a logger with optional StreamHandler and/or FileHandler. + + This function checks if a logger with the specified name already has any StreamHandler + or FileHandler attached, and if not, it adds them according to the parameters. + + Args: + name (str): The name of the logger to configure. + level (int): The logging level to set for the logger. Defaults to logging.INFO. + log_file (Optional[str]): If provided, logs will be written to this file. + use_stdout (bool): Whether to log to standard output. Defaults to True. + + Returns: + logging.Logger: A configured logger instance. + """ + logger: logging.Logger = logging.getLogger(name) + + logger.setLevel(level) + + formatter = logging.Formatter( + '[%(asctime)s]\t%(levelname)s\t%(name)s:%(lineno)d\t%(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + if use_stdout and not any(isinstance(h, logging.StreamHandler) for h in logger.handlers): + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + if log_file and not any(isinstance(h, logging.FileHandler) for h in logger.handlers): + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + logger.propagate = False + return logger From 934861149bda3a207085712c419a08d65088c788 Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:38:31 -0500 Subject: [PATCH 4/9] updated tests --- src/apiary/tests/__init__.py | 0 src/apiary/tests/conftest.py | 47 + .../test_broker/test_integration_broker.py | 14 + .../test_broker/test_unit_asyncbroker.py | 17 + .../tests/test_broker/test_unit_broker.py | 67 + .../test_unit_elasticsearch.py | 67 + .../test_flashpoint_search_fraud_vcr.yaml | 66 + .../test_integration_flashpoint.py | 11 + .../test_unit_async_flashpoint.py | 49 + .../test_flashpoint/test_unit_flashpoint.py | 45 + .../test_generic_get_github_api.yaml | 87 + .../test_integration_generic_connector.py | 12 + .../test_unit_async_generic_connector.py | 54 + .../test_unit_generic_connector.py | 34 + src/apiary/tests/test_ipqs/__init__.py | 0 .../test_ipqs_malicious_url_vcr.yaml | 64 + .../tests/test_ipqs/test_integration_ipqs.py | 13 + .../tests/test_ipqs/test_unit_async_ipqs.py | 53 + src/apiary/tests/test_ipqs/test_unit_ipqs.py | 45 + .../test_mongodb/test_unit_async_mongo.py | 109 + .../tests/test_mongodb/test_unit_mongo.py | 219 ++ src/apiary/tests/test_odbc/test_unit_odbc.py | 82 + .../tests/test_splunk/test_unit_splunk.py | 56 + .../test_spycloud_ato_search_vcr.yaml | 1870 +++++++++++++++++ .../test_integration_spycloud.py | 12 + .../test_spycloud/test_unit_async_spycloud.py | 44 + .../tests/test_spycloud/test_unit_spycloud.py | 46 + .../cassettes/test_lookup_phone_vcr.yaml | 68 + .../test_twilio/test_integration_twilio.py | 14 + .../test_twilio/test_unit_async_twilio.py | 34 + .../tests/test_twilio/test_unit_twilio.py | 45 + .../cassettes/test_urlscan_results_vcr.yaml | 279 +++ .../test_urlscan/test_integration_urlscan.py | 12 + .../test_urlscan/test_unit_async_urlscan.py | 49 + .../tests/test_urlscan/test_unit_urlscan.py | 39 + 35 files changed, 3723 insertions(+) create mode 100644 src/apiary/tests/__init__.py create mode 100644 src/apiary/tests/conftest.py create mode 100644 src/apiary/tests/test_broker/test_integration_broker.py create mode 100644 src/apiary/tests/test_broker/test_unit_asyncbroker.py create mode 100644 src/apiary/tests/test_broker/test_unit_broker.py create mode 100644 src/apiary/tests/test_elasticsearch/test_unit_elasticsearch.py create mode 100644 src/apiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml create mode 100644 src/apiary/tests/test_flashpoint/test_integration_flashpoint.py create mode 100644 src/apiary/tests/test_flashpoint/test_unit_async_flashpoint.py create mode 100644 src/apiary/tests/test_flashpoint/test_unit_flashpoint.py create mode 100644 src/apiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml create mode 100644 src/apiary/tests/test_generic/test_integration_generic_connector.py create mode 100644 src/apiary/tests/test_generic/test_unit_async_generic_connector.py create mode 100644 src/apiary/tests/test_generic/test_unit_generic_connector.py create mode 100644 src/apiary/tests/test_ipqs/__init__.py create mode 100644 src/apiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml create mode 100644 src/apiary/tests/test_ipqs/test_integration_ipqs.py create mode 100644 src/apiary/tests/test_ipqs/test_unit_async_ipqs.py create mode 100644 src/apiary/tests/test_ipqs/test_unit_ipqs.py create mode 100644 src/apiary/tests/test_mongodb/test_unit_async_mongo.py create mode 100644 src/apiary/tests/test_mongodb/test_unit_mongo.py create mode 100644 src/apiary/tests/test_odbc/test_unit_odbc.py create mode 100644 src/apiary/tests/test_splunk/test_unit_splunk.py create mode 100644 src/apiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml create mode 100644 src/apiary/tests/test_spycloud/test_integration_spycloud.py create mode 100644 src/apiary/tests/test_spycloud/test_unit_async_spycloud.py create mode 100644 src/apiary/tests/test_spycloud/test_unit_spycloud.py create mode 100644 src/apiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml create mode 100644 src/apiary/tests/test_twilio/test_integration_twilio.py create mode 100644 src/apiary/tests/test_twilio/test_unit_async_twilio.py create mode 100644 src/apiary/tests/test_twilio/test_unit_twilio.py create mode 100644 src/apiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml create mode 100644 src/apiary/tests/test_urlscan/test_integration_urlscan.py create mode 100644 src/apiary/tests/test_urlscan/test_unit_async_urlscan.py create mode 100644 src/apiary/tests/test_urlscan/test_unit_urlscan.py diff --git a/src/apiary/tests/__init__.py b/src/apiary/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apiary/tests/conftest.py b/src/apiary/tests/conftest.py new file mode 100644 index 0000000..b2b9b67 --- /dev/null +++ b/src/apiary/tests/conftest.py @@ -0,0 +1,47 @@ +import pytest +import vcr +from pathlib import Path + +AUTH_PARAM_REDACT = [ + # List of substrings that, if present in a key, will cause redaction (case-insensitive) + "key", "api_key", "access_token", "auth", "authorization", "user", "pass", "api", "x-api", "x_api" +] + + +# Redact sensitive values from request body parameters +import json +def redact_sensitive(request): + """Redact sensitive values from request body parameters.""" + if request.body: + try: + body = json.loads(request.body) + for k in list(body.keys()): + if any(redact_key.lower() in k.lower() for redact_key in AUTH_PARAM_REDACT): + body[k] = "REDACTED" + request.body = json.dumps(body) + except Exception: + pass # skip non-JSON or malformed bodies + + if request.headers: + for k in list(request.headers.keys()): + if any(redact_key.lower() in k.lower() for redact_key in AUTH_PARAM_REDACT): + request.headers[k] = "REDACTED" + + return request + + +@pytest.fixture +def vcr_cassette(request): + """Provides a VCR instance that stores cassettes in a test-local 'cassettes' folder.""" + test_dir = Path(request.module.__file__).parent + cassette_dir = test_dir / "cassettes" + + config = { + "cassette_library_dir": str(cassette_dir), + "path_transformer": vcr.VCR.ensure_suffix(".yaml"), + "record_mode": "once", + "filter_headers": [("Authorization", "DUMMY")], + "before_record": redact_sensitive + } + + return vcr.VCR(**config) \ No newline at end of file diff --git a/src/apiary/tests/test_broker/test_integration_broker.py b/src/apiary/tests/test_broker/test_integration_broker.py new file mode 100644 index 0000000..2cfda86 --- /dev/null +++ b/src/apiary/tests/test_broker/test_integration_broker.py @@ -0,0 +1,14 @@ +import httpx +from apiary.api_connectors.broker import Broker + +def test_integration_get_with_mock_transport(): + def handler(request): + assert request.url.path == "/hello" + return httpx.Response(200, json={"msg": "ok"}) + + transport = httpx.MockTransport(handler) + broker = Broker(base_url="https://testserver", timeout=5) + broker.session = httpx.Client(transport=transport) + response = broker.get("/hello") + assert response.status_code == 200 + assert response.json() == {"msg": "ok"} diff --git a/src/apiary/tests/test_broker/test_unit_asyncbroker.py b/src/apiary/tests/test_broker/test_unit_asyncbroker.py new file mode 100644 index 0000000..924fb19 --- /dev/null +++ b/src/apiary/tests/test_broker/test_unit_asyncbroker.py @@ -0,0 +1,17 @@ +import pytest +import httpx +from apiary.api_connectors.broker import AsyncBroker + + +def test_asyncbroker_rejects_mounts(): + """Ensure that AsyncBroker raises an error if 'mounts' is passed.""" + with pytest.raises(ValueError, match="mounts.*not supported"): + AsyncBroker(base_url="https://example.com", mounts={"http://": object()}) + + +def test_asyncbroker_explicit_proxy_used(mocker): + """Ensure that an explicitly provided proxy is passed to the AsyncClient.""" + client_mock = mocker.patch("httpx.AsyncClient") + AsyncBroker(base_url="https://example.com", proxy="http://explicit:1234") + client_mock.assert_called_once() + assert client_mock.call_args.kwargs["proxy"] == "http://explicit:1234" \ No newline at end of file diff --git a/src/apiary/tests/test_broker/test_unit_broker.py b/src/apiary/tests/test_broker/test_unit_broker.py new file mode 100644 index 0000000..b960e6a --- /dev/null +++ b/src/apiary/tests/test_broker/test_unit_broker.py @@ -0,0 +1,67 @@ +import pytest +from apiary.api_connectors.broker import Broker + +def test_get_calls_make_request(mocker): + broker = Broker(base_url="https://example.com") + mock_request = mocker.patch.object(broker, "_make_request") + broker.get("/test", params={"foo": "bar"}) + mock_request.assert_called_once_with("GET", "/test", params={"foo": "bar"}) + +def test_post_calls_make_request(mocker): + broker = Broker(base_url="https://example.com") + mock_request = mocker.patch.object(broker, "_make_request") + broker.post("/submit", json={"data": 123}) + mock_request.assert_called_once_with("POST", "/submit", json={"data": 123}) + +def test_logging_enabled_logs_message(mocker): + mock_logger = mocker.MagicMock() + mock_setup_logger = mocker.patch("apiary.api_connectors.broker.setup_logger", return_value=mock_logger) + broker = Broker(base_url="https://example.com", enable_logging=True) + broker._log("test message") + mock_logger.info.assert_called_once_with("test message") + +def test_env_only_proxy_from_dotenv(monkeypatch, mocker): + # Simulate .env via env_config when load_env_vars=True + mock_combine = mocker.patch( + "apiary.api_connectors.broker.combine_env_configs", + return_value={"HTTPS_PROXY": "http://env-proxy:8080"} + ) + broker = Broker(base_url="https://example.com", load_env_vars=True, trust_env=False) + assert broker.proxy == "http://env-proxy:8080" + assert broker.mounts is None + mock_combine.assert_called_once() + +def test_env_only_proxy_from_osenv(monkeypatch): + # Simulate OS env with trust_env=True + monkeypatch.setenv("HTTPS_PROXY", "http://os-proxy:9090") + broker = Broker(base_url="https://example.com", load_env_vars=False, trust_env=True) + assert broker.proxy == "http://os-proxy:9090" + assert broker.mounts is None + +def test_env_per_scheme_mounts(monkeypatch): + # Different HTTP and HTTPS proxies from env + monkeypatch.setenv("HTTP_PROXY", "http://http-proxy:8000") + monkeypatch.setenv("HTTPS_PROXY", "http://https-proxy:9000") + broker = Broker(base_url="https://example.com", load_env_vars=False, trust_env=True) + assert broker.proxy is None + assert "http://" in broker.mounts and "https://" in broker.mounts + +def test_priority_mounts_over_proxy(monkeypatch): + # If mounts are provided, they win + mounts = {"http://": object(), "https://": object()} + broker = Broker(base_url="https://example.com", mounts=mounts, proxy="http://should-not-use:1111") + assert broker.mounts == mounts + assert broker.proxy == "http://should-not-use:1111" # Stored but not used for session if mounts present + +def test_priority_proxy_over_env(monkeypatch): + monkeypatch.setenv("HTTPS_PROXY", "http://os-proxy:9999") + broker = Broker(base_url="https://example.com", proxy="http://explicit-proxy:7777", trust_env=True) + assert broker.proxy == "http://explicit-proxy:7777" + +def test_no_proxy_when_nothing_set(monkeypatch): + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + broker = Broker(base_url="https://example.com", load_env_vars=False, trust_env=False) + assert broker.proxy is None + assert broker.mounts is None diff --git a/src/apiary/tests/test_elasticsearch/test_unit_elasticsearch.py b/src/apiary/tests/test_elasticsearch/test_unit_elasticsearch.py new file mode 100644 index 0000000..a081d1e --- /dev/null +++ b/src/apiary/tests/test_elasticsearch/test_unit_elasticsearch.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +from apiary.dbms_connectors.elasticsearch import ElasticsearchConnector + +class TestElasticsearchConnector(unittest.TestCase): + + def setUp(self): + self.mock_logger = MagicMock() + self.connector = ElasticsearchConnector( + hosts=["http://localhost:9200"], + username="user", + password="pass", + logger=self.mock_logger + ) + + @patch("apiary.dbms_connectors.elasticsearch.Elasticsearch") + def test_initialization(self, mock_es): + ElasticsearchConnector( + hosts=["http://localhost:9200"], + username="user", + password="pass", + logger=self.mock_logger + ) + mock_es.assert_called_with(["http://localhost:9200"], basic_auth=("user", "pass")) + + @patch("apiary.dbms_connectors.elasticsearch.Elasticsearch") + def test_query_scroll(self, mock_es): + mock_client = MagicMock() + mock_client.search.return_value = { + "_scroll_id": "abc123", + "hits": {"hits": [{"_id": 1}, {"_id": 2}]} + } + mock_client.scroll.side_effect = [ + {"_scroll_id": "abc123", "hits": {"hits": [{"_id": 3}]}}, + {"_scroll_id": "abc123", "hits": {"hits": []}}, + ] + mock_es.return_value = mock_client + connector = ElasticsearchConnector( + hosts=["http://localhost:9200"], + username="user", + password="pass", + logger=self.mock_logger + ) + results = list(connector.query(index="test", query={"match_all": {}})) + self.assertEqual(len(results), 3) + self.assertEqual(results[0]["_id"], 1) + + @patch("apiary.dbms_connectors.elasticsearch.helpers.bulk") + def test_bulk_insert(self, mock_bulk): + mock_bulk.return_value = (3, []) + data = [{"_id": "1", "name": "Alice"}, {"_id": "2", "name": "Bob"}, {"_id": "3", "name": "Charlie"}] + success, errors = self.connector.bulk_insert(index="test-index", data=data) + self.assertEqual(success, 3) + self.assertEqual(errors, []) + self.mock_logger.info.assert_called_with("Bulk insert completed successfully") + + @patch("apiary.dbms_connectors.elasticsearch.helpers.bulk") + def test_bulk_insert_with_errors(self, mock_bulk): + mock_bulk.return_value = (2, [{"error": "failed to insert"}]) + data = [{"_id": "1", "name": "Alice"}, {"_id": "2", "name": "Bob"}] + success, errors = self.connector.bulk_insert(index="test-index", data=data) + self.assertEqual(success, 2) + self.assertTrue(errors) + self.mock_logger.error.assert_called() + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/apiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml b/src/apiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml new file mode 100644 index 0000000..f98fe7e --- /dev/null +++ b/src/apiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml @@ -0,0 +1,66 @@ +interactions: +- request: + body: '{"query": "dark web"}' + headers: + Authorization: + - DUMMY + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '20' + content-type: + - application/json + host: + - api.flashpoint.io + user-agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.flashpoint.io/sources/v2/fraud + response: + body: + string: !!binary | + H4sIAAAAAAAAA6tWyixJzS1WsoqO1VEqzqxKVbIy0FEqyS9JzFGyqlYqS8wphQgVpeYklmTm5ylZ + Kdkq1dYCAEK/yh84AAAA + headers: + CF-RAY: + - 95d9b8290dfc137a-MEM + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 11 Jul 2025 16:36:22 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Transfer-Encoding: + - chunked + cf-cache-status: + - DYNAMIC + ratelimit-limit: + - '5' + ratelimit-remaining: + - '4' + ratelimit-reset: + - '1' + via: + - kong/2.8.3 + x-kong-proxy-latency: + - '6' + x-kong-upstream-latency: + - '84' + x-ratelimit-limit-second: + - '5' + x-ratelimit-remaining-second: + - '4' + status: + code: 200 + message: OK +version: 1 diff --git a/src/apiary/tests/test_flashpoint/test_integration_flashpoint.py b/src/apiary/tests/test_flashpoint/test_integration_flashpoint.py new file mode 100644 index 0000000..670faeb --- /dev/null +++ b/src/apiary/tests/test_flashpoint/test_integration_flashpoint.py @@ -0,0 +1,11 @@ +import httpx +import pytest +from apiary.api_connectors.flashpoint import FlashpointConnector + +@pytest.mark.integration +def test_flashpoint_search_fraud_vcr(vcr_cassette): + with vcr_cassette.use_cassette("test_flashpoint_search_fraud_vcr"): + connector = FlashpointConnector(load_env_vars=True) + result = connector.search_fraud("dark web") + assert isinstance(result, httpx.Response) + assert "items" in result.json() \ No newline at end of file diff --git a/src/apiary/tests/test_flashpoint/test_unit_async_flashpoint.py b/src/apiary/tests/test_flashpoint/test_unit_async_flashpoint.py new file mode 100644 index 0000000..18770c0 --- /dev/null +++ b/src/apiary/tests/test_flashpoint/test_unit_async_flashpoint.py @@ -0,0 +1,49 @@ +import pytest +import httpx +from unittest.mock import patch, AsyncMock +from apiary.api_connectors.flashpoint import AsyncFlashpointConnector + + +@pytest.mark.asyncio +async def test_async_init_with_api_key(): + connector = AsyncFlashpointConnector(api_key="test_token") + assert connector.api_key == "test_token" + assert connector.headers["Authorization"] == "Bearer test_token" + + +@patch.dict("os.environ", {"FLASHPOINT_API_KEY": "env_token"}, clear=True) +@pytest.mark.asyncio +async def test_async_init_with_env_key(): + connector = AsyncFlashpointConnector(load_env_vars=True) + assert connector.api_key == "env_token" + assert connector.headers["Authorization"] == "Bearer env_token" + + +@pytest.mark.asyncio +async def test_async_init_missing_key(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError, match="FLASHPOINT_API_KEY is required"): + AsyncFlashpointConnector() + + +@patch("apiary.api_connectors.flashpoint.AsyncFlashpointConnector.post", new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_async_search_fraud(mock_post): + import json + + request = httpx.Request("POST", "https://api.flashpoint.io/mock") + payload = {"success": True, "data": []} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_post.return_value = mock_response + + connector = AsyncFlashpointConnector(api_key="mock_token") + result = await connector.search_fraud("credential stuffing") + + assert isinstance(result, httpx.Response) + assert result.json() == payload + mock_post.assert_awaited_once() \ No newline at end of file diff --git a/src/apiary/tests/test_flashpoint/test_unit_flashpoint.py b/src/apiary/tests/test_flashpoint/test_unit_flashpoint.py new file mode 100644 index 0000000..3ec9191 --- /dev/null +++ b/src/apiary/tests/test_flashpoint/test_unit_flashpoint.py @@ -0,0 +1,45 @@ +import httpx +import pytest +from unittest.mock import patch, MagicMock +from apiary.api_connectors.flashpoint import FlashpointConnector + +def test_init_with_api_key(): + connector = FlashpointConnector(api_key="test_token") + assert connector.api_key == "test_token" + assert connector.headers["Authorization"] == "Bearer test_token" + + +@patch.dict("os.environ", {"FLASHPOINT_API_KEY": "env_token"}, clear=True) +def test_init_with_env_key(): + connector = FlashpointConnector(load_env_vars=True) + assert connector.api_key == "env_token" + assert connector.headers["Authorization"] == "Bearer env_token" + + +def test_init_missing_key(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError, match="FLASHPOINT_API_KEY is required"): + FlashpointConnector() + + +@patch("apiary.api_connectors.flashpoint.FlashpointConnector.post") +def test_search_fraud(mock_post): + import json + + # Use a real httpx.Response so our test matches the new return type + request = httpx.Request("POST", "https://api.flashpoint.io/mock") + payload = {"success": True, "data": []} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_post.return_value = mock_response + + connector = FlashpointConnector(api_key="mock_token") + result = connector.search_fraud("credential stuffing") + + assert isinstance(result, httpx.Response) + assert result.json() == payload + mock_post.assert_called_once() \ No newline at end of file diff --git a/src/apiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml b/src/apiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml new file mode 100644 index 0000000..825a3d7 --- /dev/null +++ b/src/apiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml @@ -0,0 +1,87 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.github.com/ + response: + body: + string: !!binary | + H4sIAAAAAAAAA5VWy27jMAy85yuCHHoKqnuAop9iKDZja1e2XIlukQr591KisrHdbWVd8gBmxOGQ + Gtvv9vtDPVkLA1aTA1tNVh9O+0OHOLqTEHJUz63Cbjo/16YXAXI4fiPJCTtj1adEZQZXddjr9UGz + QxwgqqF1ojbDAHXkUKFRq5oP8KLWKihSzY2rrQr8LnIJTnJNA5UDaeturWzVIoNIWwOvby/+bQJ7 + vfmnUbZwHMmg+MMZi0djG7BJIJnTKyysECiuoAj0UmmX0R9GJBjJrUNv/qgci0GJ8E7eZwkRxIQL + QJPDR0yCG63NB9gcJXZyuYPnXNqeLS4wl8BeoLQtYBpWq1y2v4jxInw91pCuQaYuIViocm4q27jI + KFkHJmQEMYg1/YXrJs8DjhlankEXbXVkzLp4sjAap9DYK/n44hd/1/cqzWcwqC73MMj0t8CyaGNb + OaQwyrAJ6oSnz1R5Qf0nVUHOtsc5IrL8K15HyotvofG/OgiyLygQ4dzpOJ0pNKvN+yyYwGQrESqt + Qmz9nqcPYCI+JpohBiDZ+zFQToo4+tT/bAtKQnk+kYLcXDzgCqYaA2jTPB3li4UmY2Q8L0G9YFt8 + LJBsuR+zfaCJwYNBM9I2lBgaGT9nTpK18cWARh2Qc9L8PuVWPLoTLhI3E4sWDuuuYNvQYoUStwLh + Z7MWLwW72+4L1rGgQFwJAAA= + headers: + Accept-Ranges: + - bytes + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Length: + - '530' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 11 Jul 2025 16:41:44 GMT + ETag: + - '"4f825cc84e1c733059d46e76e6df9db557ae5254f9625dfe8e1b09499c449438"' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept,Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - CBC0:27B4C:44C024C:8AF37AD:68713ED1 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '59' + X-RateLimit-Reset: + - '1752255713' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + status: + code: 200 + message: OK +version: 1 diff --git a/src/apiary/tests/test_generic/test_integration_generic_connector.py b/src/apiary/tests/test_generic/test_integration_generic_connector.py new file mode 100644 index 0000000..3509569 --- /dev/null +++ b/src/apiary/tests/test_generic/test_integration_generic_connector.py @@ -0,0 +1,12 @@ +import pytest +import vcr +from apiary.api_connectors.generic import GenericConnector + +@pytest.mark.integration +def test_generic_get_github_api(vcr_cassette): + with vcr_cassette.use_cassette("test_generic_get_github_api"): + connector = GenericConnector(base_url="https://api.github.com") + response = connector.get("/") + + data = response.json() + assert "current_user_url" in data diff --git a/src/apiary/tests/test_generic/test_unit_async_generic_connector.py b/src/apiary/tests/test_generic/test_unit_async_generic_connector.py new file mode 100644 index 0000000..f916c96 --- /dev/null +++ b/src/apiary/tests/test_generic/test_unit_async_generic_connector.py @@ -0,0 +1,54 @@ +import pytest +import httpx +from unittest.mock import AsyncMock, patch +from apiary.api_connectors.generic import AsyncGenericConnector + + +@pytest.mark.asyncio +async def test_async_init_sets_base_url(): + connector = AsyncGenericConnector(base_url="https://example.com") + assert connector.base_url == "https://example.com" + + +@patch("apiary.api_connectors.generic.AsyncGenericConnector._make_request", new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_async_get_request(mock_make_request): + mock_response = httpx.Response(200, json={"result": "ok"}) + mock_make_request.return_value = mock_response + + connector = AsyncGenericConnector(base_url="https://example.com") + response = await connector.request("GET", "/test") + + assert response.status_code == 200 + assert response.json() == {"result": "ok"} + mock_make_request.assert_awaited_once_with( + method="GET", + endpoint="/test", + headers=connector.headers, + params=None, + json=None, + auth=None, + retry_kwargs=None, + ) + + +@patch("apiary.api_connectors.generic.AsyncGenericConnector._make_request", new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_async_post_request(mock_make_request): + mock_response = httpx.Response(200, json={"posted": True}) + mock_make_request.return_value = mock_response + + connector = AsyncGenericConnector(base_url="https://example.com") + response = await connector.request("POST", "/submit", json={"key": "value"}) + + assert response.status_code == 200 + assert response.json() == {"posted": True} + mock_make_request.assert_awaited_once_with( + method="POST", + endpoint="/submit", + headers=connector.headers, + params=None, + json={"key": "value"}, + auth=None, + retry_kwargs=None, + ) \ No newline at end of file diff --git a/src/apiary/tests/test_generic/test_unit_generic_connector.py b/src/apiary/tests/test_generic/test_unit_generic_connector.py new file mode 100644 index 0000000..0ab2985 --- /dev/null +++ b/src/apiary/tests/test_generic/test_unit_generic_connector.py @@ -0,0 +1,34 @@ +import pytest +from unittest.mock import patch, MagicMock +from apiary.api_connectors.generic import GenericConnector + + +def test_init_sets_base_url(): + connector = GenericConnector(base_url="https://example.com") + assert connector.base_url == "https://example.com" + + +@patch("apiary.api_connectors.generic.GenericConnector._make_request") +def test_get_request(mock_make_request): + mock_response = MagicMock() + mock_response.json.return_value = {"result": "ok"} + mock_make_request.return_value = mock_response + + connector = GenericConnector(base_url="https://example.com") + response = connector.get("/test") + + assert response.json() == {"result": "ok"} + mock_make_request.assert_called_once_with("GET", "/test", params=None) + + +@patch("apiary.api_connectors.generic.GenericConnector._make_request") +def test_post_request(mock_make_request): + mock_response = MagicMock() + mock_response.json.return_value = {"posted": True} + mock_make_request.return_value = mock_response + + connector = GenericConnector(base_url="https://example.com") + response = connector.post("/submit", json={"key": "value"}) + + assert response.json() == {"posted": True} + mock_make_request.assert_called_once_with("POST", "/submit", json={"key": "value"}) diff --git a/src/apiary/tests/test_ipqs/__init__.py b/src/apiary/tests/test_ipqs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml b/src/apiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml new file mode 100644 index 0000000..85fb2e0 --- /dev/null +++ b/src/apiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: '{"url": "github.com", "key": "REDACTED"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '61' + content-type: + - application/json + host: + - ipqualityscore.com + user-agent: + - python-httpx/0.28.1 + method: POST + uri: https://ipqualityscore.com/api/json/url/ + response: + body: + string: !!binary | + H4sIAAAAAAAAA3RUTW/bMAz9KwIPOzmu7DhN4mGXDcO2yy7dDtsaGIyt2EJkyROptlnR/z6ocT76 + dbL4HkXykbTuoVdE2Coo4SrUtSJKIQHaH6FkH1QCwRJuFJQbNKQSaFyP2kIJreYurNPa9ZCAd46r + 1yk9VNg0/jEiZIVMF3maZUVaxFTK3yj//EbtLCvLFe+GWBqrO76+6Lg370XdoSfFHwJvJosYgJED + VbVrFJS5lAkM2KqK9L9oL2bLy+Wh5Mqj3UI5nSfQWKpu0OjmoHFAv9W2PYqkAfv+HOjR3KI/dWHo + NHVPbgQadK1doCOETTB8tLymbUW1i0Fk1Bgs+91YOfy8ggQM2jbE8kfw8/fYDGTVOr+DEj65fgis + PIl34ptl5a1iOMp7nOM9dKHHOINsIXYKPQlsHSTAulfE2A9QZtkyW84KOZMJaHJQQi7lfJLJiVz+ + yIoyl+VMTmRRSgkPx/DsA3GcRvyqJs5cNdqrOhpjG6lzniuj7bY6kEf9nYvXqnG2p0ZGvazZRMFf + NH8NayE+Bm0agbYR1OlBkNtw7L5wVqAgbVujElE7Y3DtPLK+UWIwyBvneyH2QWDf8V3Fpjkb66by + qnb+BDU9+voI7mWwqjvrjGu1Iij/rBLA0SOaT3d4lUB/d86i4WmKNPR3qUlb51qjxrVGw8WbTPYm + k7/BvARXCVg6r6WxlKeDXKSWnFXpuC6Wpq+BxQvQ0iTLF9MUb6mxNJnmqfPtiM/l/IDnWVq7NGz3 + TJFnB2KWj7U+nuUBltkpafYs6SoBqtFa1VTBGyihYx7K64vriycvxEZbNGce9NLFq79BEVfxJ4df + 2/Vlq3/fhgwe/gMAAP//AwB3HyEu9gQAAA== + headers: + CF-RAY: + - 95d29762d81310be-ORD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Thu, 10 Jul 2025 19:50:40 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=cnp43vf6ymYDAXC9fg%2FhY3TA2duBQq5GEDBoldh1PtroZ77jm2tQbOO7z5x%2Ffig70pMLaIIJmjnULNTy5Ts1IGQe4CgYtA244Lz7XwHYtCbbyHWOXBYB72ebfp50XXiPyMMb9g%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + server-timing: + - cfL4;desc="?proto=TCP&rtt=35415&min_rtt=33540&rtt_var=13917&sent=5&recv=9&lost=0&retrans=0&sent_bytes=3948&recv_bytes=1937&delivery_rate=129516&cwnd=38&unsent_bytes=0&cid=ff31fc6046b459f4&ts=565&x=0" + status: + code: 200 + message: OK +version: 1 diff --git a/src/apiary/tests/test_ipqs/test_integration_ipqs.py b/src/apiary/tests/test_ipqs/test_integration_ipqs.py new file mode 100644 index 0000000..01eb673 --- /dev/null +++ b/src/apiary/tests/test_ipqs/test_integration_ipqs.py @@ -0,0 +1,13 @@ +import httpx +import pytest +from apiary.api_connectors.ipqs import IPQSConnector + +@pytest.mark.integration +def test_ipqs_malicious_url_vcr(vcr_cassette): + with vcr_cassette.use_cassette("test_ipqs_malicious_url_vcr"): + connector = IPQSConnector(load_env_vars=True, enable_logging=True) + result = connector.malicious_url("github.com") + + assert isinstance(result, httpx.Response) + assert "domain" in result.json() + assert result.json()["domain"] == "github.com" \ No newline at end of file diff --git a/src/apiary/tests/test_ipqs/test_unit_async_ipqs.py b/src/apiary/tests/test_ipqs/test_unit_async_ipqs.py new file mode 100644 index 0000000..df461d2 --- /dev/null +++ b/src/apiary/tests/test_ipqs/test_unit_async_ipqs.py @@ -0,0 +1,53 @@ +import pytest +import httpx +from unittest.mock import patch, AsyncMock +from apiary.api_connectors.ipqs import AsyncIPQSConnector + + +@pytest.mark.asyncio +async def test_async_init_with_api_key(): + connector = AsyncIPQSConnector(api_key="test_key") + assert connector.api_key == "test_key" + assert connector.headers["Content-Type"] == "application/json" + + +@pytest.mark.asyncio +async def test_async_init_with_env_key(): + with patch.dict("os.environ", {"IPQS_API_KEY": "env_key"}): + connector = AsyncIPQSConnector(load_env_vars=True) + assert connector.api_key == "env_key" + assert connector.headers["Content-Type"] == "application/json" + + +@pytest.mark.asyncio +async def test_async_init_missing_key(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError, match="API key is required"): + AsyncIPQSConnector() + + +@patch("apiary.api_connectors.ipqs.AsyncIPQSConnector.post", new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_async_malicious_url(mock_post): + import json + + request = httpx.Request("POST", "https://ipqualityscore.com/api/json/url/") + payload = {"success": True, "domain": "example.com"} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_post.return_value = mock_response + + connector = AsyncIPQSConnector(api_key="test_key") + response = await connector.malicious_url("example.com", strictness=1) + + assert isinstance(response, httpx.Response) + assert response.status_code == 200 + assert response.json() == payload + mock_post.assert_awaited_once_with( + "/url/", + json={"url": "example.com", "key": "test_key", "strictness": 1} + ) \ No newline at end of file diff --git a/src/apiary/tests/test_ipqs/test_unit_ipqs.py b/src/apiary/tests/test_ipqs/test_unit_ipqs.py new file mode 100644 index 0000000..9bad35c --- /dev/null +++ b/src/apiary/tests/test_ipqs/test_unit_ipqs.py @@ -0,0 +1,45 @@ +import httpx +import pytest +from unittest.mock import patch, MagicMock +from apiary.api_connectors.ipqs import IPQSConnector + + +def test_init_with_api_key(): + connector = IPQSConnector(api_key="test_key") + assert connector.api_key == "test_key" + assert connector.headers["Content-Type"] == "application/json" + + +def test_init_with_env_key(): + with patch.dict("os.environ", {"IPQS_API_KEY": "env_key"}): + connector = IPQSConnector(load_env_vars=True) + assert connector.api_key == "env_key" + + +def test_init_missing_key(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError, match="API key is required"): + IPQSConnector() + + +@patch("apiary.api_connectors.ipqs.IPQSConnector.post") +def test_malicious_url(mock_post): + # Build a real httpx.Response to match the new return type + import json + + request = httpx.Request("POST", "https://ipqualityscore.com/api/json/url/") + payload = {"success": True, "domain": "example.com"} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_post.return_value = mock_response + + connector = IPQSConnector(api_key="test_key") + result = connector.malicious_url("example.com") + + mock_post.assert_called_once() + assert isinstance(result, httpx.Response) + assert result.json() == payload diff --git a/src/apiary/tests/test_mongodb/test_unit_async_mongo.py b/src/apiary/tests/test_mongodb/test_unit_async_mongo.py new file mode 100644 index 0000000..e47add0 --- /dev/null +++ b/src/apiary/tests/test_mongodb/test_unit_async_mongo.py @@ -0,0 +1,109 @@ +import sys +import types +import pytest +from unittest.mock import AsyncMock + +from apiary.dbms_connectors.mongo_async import AsyncMongoConnector + + +@pytest.mark.asyncio +async def test_async_init_and_ping(monkeypatch): + fake_client = types.SimpleNamespace() + fake_client.admin = types.SimpleNamespace() + fake_client.admin.command = AsyncMock(return_value={"ok": 1}) + + # Provide pymongo.asyncio.MongoClient for import inside the connector + ns = types.SimpleNamespace(AsyncMongoClient=lambda *a, **k: fake_client) + monkeypatch.setitem(sys.modules, "pymongo.asynchronous.mongo_client", ns) + + async with AsyncMongoConnector(uri="mongodb://localhost:27017") as conn: + assert isinstance(conn, AsyncMongoConnector) + + fake_client.admin.command.assert_awaited_once_with("ping") + + +class _AsyncCursor: + def __init__(self, items): + self._items = items + self._idx = 0 + + def batch_size(self, n): + return self + + def __aiter__(self): + self._idx = 0 + return self + + async def __anext__(self): + if self._idx >= len(self._items): + raise StopAsyncIteration + item = self._items[self._idx] + self._idx += 1 + return item + + +@pytest.mark.asyncio +async def test_async_find(monkeypatch): + docs = [{"_id": 1}, {"_id": 2}] + + class _FakeCollection: + def find(self, f, p): + return _AsyncCursor(docs) + + class _FakeDB(dict): + def __getitem__(self, k): + return _FakeCollection() + + class _FakeClient: + def __init__(self): + self.admin = types.SimpleNamespace() + self.admin.command = AsyncMock(return_value={"ok": 1}) + + def __getitem__(self, k): + return _FakeDB() + + def close(self): + pass + + fake_client = _FakeClient() + ns = types.SimpleNamespace(AsyncMongoClient=lambda *a, **k: fake_client) + monkeypatch.setitem(sys.modules, "pymongo.asynchronous.mongo_client", ns) + + async with AsyncMongoConnector(uri="mongodb://localhost:27017") as conn: + out = [doc async for doc in conn.find("db", "col", filter={})] + + assert out == docs + + +@pytest.mark.asyncio +async def test_async_aggregate(monkeypatch): + docs = [{"_id": 1, "count": 2}, {"_id": 2, "count": 3}] + + class _FakeCollection: + def aggregate(self, pipeline, **kwargs): + return _AsyncCursor(docs) + + class _FakeDB(dict): + def __getitem__(self, k): + return _FakeCollection() + + class _FakeClient: + def __init__(self): + self.admin = types.SimpleNamespace() + self.admin.command = AsyncMock(return_value={"ok": 1}) + + def __getitem__(self, k): + return _FakeDB() + + def close(self): + pass + + fake_client = _FakeClient() + ns = types.SimpleNamespace(AsyncMongoClient=lambda *a, **k: fake_client) + monkeypatch.setitem(sys.modules, "pymongo.asynchronous.mongo_client", ns) + + out = [] + async with AsyncMongoConnector(uri="mongodb://localhost:27017") as conn: + out = [doc async for doc in conn.aggregate("db", "col", pipeline=[{"$match": {}}], batch_size=50)] + + assert out == docs diff --git a/src/apiary/tests/test_mongodb/test_unit_mongo.py b/src/apiary/tests/test_mongodb/test_unit_mongo.py new file mode 100644 index 0000000..17d5d71 --- /dev/null +++ b/src/apiary/tests/test_mongodb/test_unit_mongo.py @@ -0,0 +1,219 @@ +import pytest +from pymongo import UpdateOne +from pymongo.errors import ServerSelectionTimeoutError +from apiary.dbms_connectors.mongo import MongoConnector +from unittest.mock import patch, MagicMock + + +def test_mongo_query(monkeypatch): + # Prevent real connection by mocking MongoClient and returning a successful ping + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + mock_cursor = [ + {"_id": 1, "name": "Alice"}, + {"_id": 2, "name": "Bob"} + ] + mock_collection = MagicMock() + mock_collection.find.return_value.batch_size.return_value = mock_cursor + mock_db = {"test_collection": mock_collection} + mock_client = {"test_db": mock_db} + + connector = MongoConnector( + uri="mongodb://localhost:27017", + username="fake", + password="fake", + ) + connector.client = mock_client + + results = list(connector.query("test_db", "test_collection", {})) + assert results == mock_cursor + + +def test_insert_many(monkeypatch): + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + mock_insert_many = MagicMock() + mock_collection = {"insert_many": mock_insert_many} + mock_db = {"test_collection": MagicMock(return_value=mock_insert_many)} + mock_client = {"test_db": mock_db} + + connector = MongoConnector( + uri="mongodb://localhost:27017", + username="fake", + password="fake", + ) + connector.client = MagicMock() + connector.client.__getitem__.return_value.__getitem__.return_value.insert_many = mock_insert_many + + test_data = [{"_id": 1}, {"_id": 2}] + connector.insert_many("test_db", "test_collection", test_data) + + mock_insert_many.assert_called_once_with(test_data, ordered=False) + + +def test_distinct(monkeypatch): + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + mock_collection = MagicMock() + mock_collection.distinct.return_value = ["A", "B"] + connector = MongoConnector( + uri="mongodb://localhost:27017", + username="fake", + password="fake", + ssl=False + ) + connector.client = MagicMock() + connector.client.__getitem__.return_value.__getitem__.return_value = mock_collection + + values = connector.distinct("test_db", "test_collection", key="field", filter={"x": 1}, maxTimeMS=5000) + assert values == ["A", "B"] + mock_collection.distinct.assert_called_once_with("field", {"x": 1}, maxTimeMS=5000) + + +def test_aggregate(monkeypatch): + mock_cursor = [ + {"_id": 1, "count": 10}, + {"_id": 2, "count": 5}, + ] + mock_collection = MagicMock() + # aggregate().batch_size() returns an iterable cursor + mock_collection.aggregate.return_value.batch_size.return_value = mock_cursor + + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + fake_client.__getitem__.return_value.__getitem__.return_value = mock_collection + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + + connector = MongoConnector(uri="mongodb://localhost:27017") + pipeline = [{"$match": {"x": {"$gt": 1}}}, {"$group": {"_id": "$x", "count": {"$sum": 1}}}] + out = list(connector.aggregate("db", "col", pipeline, batch_size=100, allowDiskUse=True)) + assert out == mock_cursor + mock_collection.aggregate.assert_called_once_with(pipeline, allowDiskUse=True) + + +def test_mongo_connection_failure(): + with pytest.raises(ServerSelectionTimeoutError): + MongoConnector( + uri="mongodb://localhost:27018", # invalid port + username="fake", + password="fake", + timeout=1, + ) + + + +@patch("apiary.dbms_connectors.mongo.MongoClient") +def test_mongo_init_with_auth_and_ssl(mock_mongo_client): + # Mock ping success on the returned client instance + instance = MagicMock() + instance.admin.command.return_value = {"ok": 1} + mock_mongo_client.return_value = instance + + MongoConnector( + uri="mongodb://example.com:27017", + username="user", + password="pass", + auth_source="authdb", + auth_mechanism="SCRAM-SHA-1", + ssl=True + ) + mock_mongo_client.assert_called_once_with( + "mongodb://example.com:27017", + username="user", + password="pass", + authSource="authdb", + authMechanism="SCRAM-SHA-1", + ssl=True, + serverSelectionTimeoutMS=10000 + ) + + +@patch("apiary.dbms_connectors.mongo.MongoClient") +def test_mongo_init_defaults(mock_mongo_client): + instance = MagicMock() + instance.admin.command.return_value = {"ok": 1} + mock_mongo_client.return_value = instance + MongoConnector(uri="mongodb://localhost:27017") + mock_mongo_client.assert_called_once() + + + +def test_upsert_many_with_unique_key(monkeypatch): + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + mock_bulk_write = MagicMock() + mock_collection = MagicMock() + mock_collection.bulk_write = mock_bulk_write + + connector = MongoConnector( + uri="mongodb://localhost:27017", + username="fake", + password="fake", + ) + connector.client = MagicMock() + connector.client.__getitem__.return_value.__getitem__.return_value = mock_collection + + data = [{"_id": 1, "value": "A"}, {"_id": 2, "value": "B"}] + connector.upsert_many("test_db", "test_collection", data, unique_key="_id") + + ops = [ + UpdateOne({"_id": 1}, {"$set": {"_id": 1, "value": "A"}}, upsert=True), + UpdateOne({"_id": 2}, {"$set": {"_id": 2, "value": "B"}}, upsert=True) + ] + mock_bulk_write.assert_called_once_with(ops, ordered=False) + + +def test_upsert_many_raises_without_unique_key(monkeypatch): + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + connector = MongoConnector( + uri="mongodb://localhost:27017", + username="fake", + password="fake", + ) + mock_collection = MagicMock() + mock_collection.bulk_write = MagicMock() + connector.client = MagicMock() + connector.client.__getitem__.return_value.__getitem__.return_value = mock_collection + + data = [{"_id": i} for i in range(10)] + with pytest.raises(ValueError, match="unique_key must be provided for upsert_many"): + connector.upsert_many("test_db", "test_collection", data, unique_key=None) + + +def test_context_manager_closes_client(monkeypatch): + fake_client = MagicMock() + fake_client.admin.command.return_value = {"ok": 1} + monkeypatch.setattr( + "apiary.dbms_connectors.mongo.MongoClient", + MagicMock(return_value=fake_client), + ) + + with MongoConnector(uri="mongodb://localhost:27017") as conn: + assert isinstance(conn, MongoConnector) + + # After exiting context, close should be called + fake_client.close.assert_called_once() diff --git a/src/apiary/tests/test_odbc/test_unit_odbc.py b/src/apiary/tests/test_odbc/test_unit_odbc.py new file mode 100644 index 0000000..fc87acd --- /dev/null +++ b/src/apiary/tests/test_odbc/test_unit_odbc.py @@ -0,0 +1,82 @@ +from unittest.mock import MagicMock, patch + +import pytest + +import apiary.dbms_connectors.odbc as odbc_module +from apiary.dbms_connectors.odbc import ODBCConnector + + +@pytest.fixture +def mock_pyodbc(): + with patch("apiary.dbms_connectors.odbc._get_pyodbc") as mock_loader: + pyodbc_mock = MagicMock() + mock_loader.return_value = pyodbc_mock + yield pyodbc_mock + + +def test_odbcconnector_init(mock_pyodbc): + mock_logger = MagicMock() + connector = ODBCConnector("DSN=testdb", logger=mock_logger) + mock_pyodbc.connect.assert_called_once_with("DSN=testdb") + assert connector.logger == mock_logger + + +def test_odbcconnector_query_returns_rows(mock_pyodbc): + """Test that query returns rows as dictionaries.""" + mock_cursor = MagicMock() + mock_cursor.description = [("id",), ("name",)] + mock_cursor.fetchall.return_value = [(1, "Alice"), (2, "Bob")] + mock_pyodbc.connect.return_value.cursor.return_value = mock_cursor + connector = ODBCConnector("DSN=testdb") + + results = list(connector.query("SELECT * FROM users")) + + assert results == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] + + +def test_odbcconnector_bulk_insert(mock_pyodbc): + mock_cursor = MagicMock() + mock_pyodbc.connect.return_value.cursor.return_value = mock_cursor + connector = ODBCConnector("DSN=testdb") + + data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] + connector.bulk_insert("users", data) + + assert mock_cursor.executemany.called + assert mock_pyodbc.connect.return_value.commit.called + + +def test_odbcconnector_bulk_insert_empty_data(mock_pyodbc): + mock_cursor = MagicMock() + mock_pyodbc.connect.return_value.cursor.return_value = mock_cursor + connector = ODBCConnector("DSN=testdb") + + connector.bulk_insert("users", []) + + mock_cursor.executemany.assert_not_called() + + +def test_odbcconnector_context_manager_closes_connection(mock_pyodbc): + """Test that the context manager closes the connection.""" + mock_conn = MagicMock() + mock_pyodbc.connect.return_value = mock_conn + + with ODBCConnector("DSN=testdb") as connector: + assert isinstance(connector, ODBCConnector) + + mock_conn.close.assert_called_once() + + +def test_odbcconnector_raises_helpful_error_when_pyodbc_missing(monkeypatch): + """Ensure that using the connector without the extra raises a clear error.""" + monkeypatch.setattr(odbc_module, "_PYODBC_MODULE", None) + monkeypatch.setattr( + odbc_module, + "import_module", + MagicMock(side_effect=ImportError("pyodbc missing")), + ) + + with pytest.raises(ImportError) as excinfo: + ODBCConnector("DSN=testdb") + + assert "pyodbc is not installed" in str(excinfo.value) diff --git a/src/apiary/tests/test_splunk/test_unit_splunk.py b/src/apiary/tests/test_splunk/test_unit_splunk.py new file mode 100644 index 0000000..594e300 --- /dev/null +++ b/src/apiary/tests/test_splunk/test_unit_splunk.py @@ -0,0 +1,56 @@ +import pytest +import httpx +from unittest.mock import patch, MagicMock +from apiary.dbms_connectors.splunk import SplunkConnector + + +@pytest.fixture +def connector(): + return SplunkConnector( + host="localhost", + port=8089, + username="admin", + password="admin123", + verify=False + ) + + +@patch("httpx.post") +@patch("httpx.get") +def test_query_success(mock_get, mock_post, connector): + # Mock POST /services/search/jobs + post_response = MagicMock() + post_response.raise_for_status = MagicMock() + post_response.json.return_value = {"sid": "abc123"} + mock_post.return_value = post_response + + # Mock GET /services/search/jobs/{sid} + get_status_response = MagicMock() + get_status_response.raise_for_status = MagicMock() + get_status_response.json.return_value = { + "entry": [{"content": {"isDone": True}}] + } + + # Mock GET /services/search/jobs/{sid}/results + get_results_response = MagicMock() + get_results_response.raise_for_status = MagicMock() + get_results_response.json.side_effect = [ + {"results": [{"foo": "bar"}, {"baz": "qux"}]}, + {"results": []} + ] + mock_get.side_effect = [get_status_response, get_results_response, get_results_response] + + results = list(connector.query("search index=_internal | head 2")) + assert len(results) == 2 + assert results[0]["foo"] == "bar" + assert results[1]["baz"] == "qux" + + +@patch("httpx.post") +def test_query_auth_error(mock_post, connector): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = httpx.HTTPError("401 Unauthorized") + mock_post.return_value = mock_response + + with pytest.raises(httpx.HTTPError): + list(connector.query("search index=_internal | head 1")) diff --git a/src/apiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml b/src/apiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml new file mode 100644 index 0000000..11230e7 --- /dev/null +++ b/src/apiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml @@ -0,0 +1,1870 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.spycloud.io + user-agent: + - REDACTED + x-api-key: + - REDACTED + method: GET + uri: https://api.spycloud.io/sp-v2/breach/data/ips/8.8.8.8 + response: + body: + string: !!binary | + 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 + yJJrB5TvL3/6y/8HdXNTjxy/CAA= + headers: + Access-Control-Allow-Origin: + - https://spycloud-external.readme.io + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '103589' + Content-Type: + - application/json + Date: + - Fri, 11 Jul 2025 18:22:05 GMT + Via: + - 1.1 20e1ec5c4961778268603d507aa565a0.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - 9vJcGrmq2lgOxSAXTstWJGhnfPFqhl7mhH7k55kg4ExaiigLZhw3mQ== + X-Amz-Cf-Pop: + - ATL56-P2 + X-Amzn-Trace-Id: + - Root=1-6871564a-483337b646e7cfb9409e8cc7;Parent=558346f744e1c96b;Sampled=0;Lineage=2:09b13bf6:0 + X-Cache: + - Miss from cloudfront + x-amz-apigw-id: + - NjprvF9EoAMEoTA= + x-amzn-RequestId: + - 79eecd0c-ef10-4c22-9934-001ade2c8ca2 + status: + code: 200 + message: OK +version: 1 diff --git a/src/apiary/tests/test_spycloud/test_integration_spycloud.py b/src/apiary/tests/test_spycloud/test_integration_spycloud.py new file mode 100644 index 0000000..ab4e4f1 --- /dev/null +++ b/src/apiary/tests/test_spycloud/test_integration_spycloud.py @@ -0,0 +1,12 @@ +import httpx +import pytest +from apiary.api_connectors.spycloud import SpycloudConnector + +@pytest.mark.integration +def test_spycloud_ato_search_vcr(vcr_cassette): + with vcr_cassette.use_cassette("test_spycloud_ato_search_vcr"): + connector = SpycloudConnector(load_env_vars=True, enable_logging=True) + result = connector.ato_search(search_type="ip", query="8.8.8.8") + + assert isinstance(result, httpx.Response) + assert "results" in result.json() \ No newline at end of file diff --git a/src/apiary/tests/test_spycloud/test_unit_async_spycloud.py b/src/apiary/tests/test_spycloud/test_unit_async_spycloud.py new file mode 100644 index 0000000..37dec52 --- /dev/null +++ b/src/apiary/tests/test_spycloud/test_unit_async_spycloud.py @@ -0,0 +1,44 @@ +import pytest +import httpx +from apiary.api_connectors.spycloud import AsyncSpycloudConnector + +@pytest.mark.asyncio +async def test_async_init_with_env_keys(monkeypatch): + monkeypatch.setenv("SPYCLOUD_API_SIP_KEY", "sip") + monkeypatch.setenv("SPYCLOUD_API_ATO_KEY", "ato") + monkeypatch.setenv("SPYCLOUD_API_INV_KEY", "inv") + + conn = AsyncSpycloudConnector(load_env_vars=True) + assert conn.sip_key == "sip" + assert conn.ato_key == "ato" + assert conn.inv_key == "inv" + +@pytest.mark.asyncio +async def test_async_sip_cookie_domains_sends(mocker): + mock_response = mocker.AsyncMock(spec=httpx.Response) + request = mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + connector = AsyncSpycloudConnector(sip_key="abc123") + result = await connector.sip_cookie_domains("test.com", q="xyz") + + assert result is mock_response + request.assert_called_once() + args, kwargs = request.call_args + assert kwargs["url"].endswith("/sip-v1/breach/data/cookie-domains/test.com") + assert kwargs["headers"]["x-api-key"] == "abc123" + assert kwargs["params"]["q"] == "xyz" + +@pytest.mark.asyncio +async def test_async_ato_search_ip(mocker): + mock_response = mocker.AsyncMock(spec=httpx.Response) + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + connector = AsyncSpycloudConnector(ato_key="abc123") + resp = await connector.ato_search("ip", "1.2.3.4") + assert resp is mock_response + +@pytest.mark.asyncio +async def test_async_investigations_invalid_type_raises(): + conn = AsyncSpycloudConnector(inv_key="abc") + with pytest.raises(ValueError): + await conn.investigations_search("not-a-real-type", "foo") \ No newline at end of file diff --git a/src/apiary/tests/test_spycloud/test_unit_spycloud.py b/src/apiary/tests/test_spycloud/test_unit_spycloud.py new file mode 100644 index 0000000..b756ef6 --- /dev/null +++ b/src/apiary/tests/test_spycloud/test_unit_spycloud.py @@ -0,0 +1,46 @@ +import json +import httpx +import pytest +from unittest.mock import patch, MagicMock +from apiary.api_connectors.spycloud import SpycloudConnector + + +def test_init_with_api_key(): + connector = SpycloudConnector(sip_key="sip", ato_key="ato", inv_key="inv") + assert connector.sip_key == "sip" + assert connector.ato_key == "ato" + assert connector.inv_key == "inv" + + +@patch.dict("os.environ", {"SPYCLOUD_API_ATO_KEY": "env_key"}, clear=True) +def test_init_with_env_key(): + connector = SpycloudConnector(load_env_vars=True) + assert connector.ato_key == "env_key" + + +@patch("apiary.api_connectors.broker.combine_env_configs", return_value={}) +def test_init_missing_key(mock_env): + connector = SpycloudConnector(load_env_vars=True) + with pytest.raises(ValueError, match="SPYCLOUD_API_ATO_KEY is required for this request"): + connector.ato_search(search_type="email", query="test@example.com") + + +@patch("apiary.api_connectors.spycloud.SpycloudConnector._make_request") +def test_ato_search(mock_request): + # Build a real httpx.Response to match new return type (raw Response) + request = httpx.Request("GET", "https://api.spycloud.io/sp-v2/breach/data/emails/test@example.com") + payload = {"results": []} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_request.return_value = mock_response + + connector = SpycloudConnector(ato_key="test_key") + result = connector.ato_search(search_type="ip", query="test@example.com") + + mock_request.assert_called_once() + assert isinstance(result, httpx.Response) + assert result.json() == payload \ No newline at end of file diff --git a/src/apiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml b/src/apiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml new file mode 100644 index 0000000..d9d7a57 --- /dev/null +++ b/src/apiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml @@ -0,0 +1,68 @@ +interactions: +- request: + body: '' + headers: + Authorization: + - DUMMY + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - lookups.twilio.com + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://lookups.twilio.com/v2/PhoneNumbers/+14155552671?Fields= + response: + body: + string: !!binary | + H4sIAAAAAAAAAG2Q3UrEMBCFX6X0SlE3dLEK+xCLIF6JhJjOtmHz58xkSxHf3aQorcXcDDln+M5h + PmutrJWngKPCzvi+PlQ+WXtbzQag9MrBXzFvSR2SZ5zy7IpbN3UxN+LLc1FNB54NT9Ip1sOCyhyQ + xIoTbUSeIkjjGXJWD16v8r1iE7yaG2deSbm6b9rrqm3bu/3D49wjDiFTfHLvgGXjpskr+f3ny4+k + bGlHOuAqKCLIk7F2URAUkek9dAv6xyLjJI0qrhRHMiYXy63Q0HlxEhZmPTBHOghhQzinSDsejTVh + p4MTl714Kg2PcwqJbf1LLtxlBmOC3+98FgmIAcs1X6u3r29cgXV62wEAAA== + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '271' + Content-Type: + - application/json;charset=utf-8 + Date: + - Fri, 11 Jul 2025 17:18:13 GMT + Strict-Transport-Security: + - max-age=31536000 + Twilio-Concurrent-Requests: + - '1' + Twilio-Request-Duration: + - '0.072' + Twilio-Request-Id: + - RQc8a2e45336e41fa56ef57dc5d1b07623 + Vary: + - Accept-Encoding + - Origin + Via: + - 1.1 b8ac2f92eb514fa1ca7a47e834b5044e.cloudfront.net (CloudFront) + X-API-Domain: + - lookups.twilio.com + X-Amz-Cf-Id: + - -8mzkQVCupui1I0KUXPtHbjmOB3ubY0xB7iaWxCQpMjBxGQ-jAOikw== + X-Amz-Cf-Pop: + - ATL59-P9 + X-Cache: + - Miss from cloudfront + X-Home-Region: + - us1 + X-Powered-By: + - AT-5000 + X-Shenanigans: + - none + status: + code: 200 + message: OK +version: 1 diff --git a/src/apiary/tests/test_twilio/test_integration_twilio.py b/src/apiary/tests/test_twilio/test_integration_twilio.py new file mode 100644 index 0000000..22b83a7 --- /dev/null +++ b/src/apiary/tests/test_twilio/test_integration_twilio.py @@ -0,0 +1,14 @@ + +import httpx +import pytest +import vcr +from apiary.api_connectors.twilio import TwilioConnector + +@pytest.mark.integration +def test_lookup_phone_vcr(vcr_cassette): + with vcr_cassette.use_cassette("test_lookup_phone_vcr"): + connector = TwilioConnector(load_env_vars=True) + result = connector.lookup_phone("+14155552671") + + assert isinstance(result, httpx.Response) + assert "phone_number" in result.json() or "caller_name" in result.json() or "line_type" in result.json() \ No newline at end of file diff --git a/src/apiary/tests/test_twilio/test_unit_async_twilio.py b/src/apiary/tests/test_twilio/test_unit_async_twilio.py new file mode 100644 index 0000000..0805456 --- /dev/null +++ b/src/apiary/tests/test_twilio/test_unit_async_twilio.py @@ -0,0 +1,34 @@ +import pytest +import httpx +from unittest.mock import patch, AsyncMock +from apiary.api_connectors.twilio import AsyncTwilioConnector + +@pytest.mark.asyncio +async def test_async_init_with_env(monkeypatch): + monkeypatch.setenv("TWILIO_API_SID", "sid") + monkeypatch.setenv("TWILIO_API_SECRET", "secret") + + connector = AsyncTwilioConnector(load_env_vars=True) + assert connector.api_sid == "sid" + assert connector.api_secret == "secret" + +@pytest.mark.asyncio +async def test_async_lookup_phone_raises_for_invalid_packages(): + connector = AsyncTwilioConnector(api_sid="a", api_secret="b") + with pytest.raises(ValueError, match="Invalid data packages: badpkg"): + await connector.lookup_phone("+14155552671", data_packages=["badpkg"]) + +@patch("apiary.api_connectors.twilio.AsyncTwilioConnector._make_request", new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_async_lookup_phone_calls_make_request(mock_request): + mock_resp = httpx.Response(200, content=b'{"valid": true}') + mock_request.return_value = mock_resp + + connector = AsyncTwilioConnector(api_sid="a", api_secret="b") + result = await connector.lookup_phone("+14155552671", data_packages=["caller_name"]) + + assert result.status_code == 200 + assert mock_request.await_count == 1 + args, kwargs = mock_request.call_args + assert kwargs["endpoint"].endswith("+14155552671") + assert "caller_name" in kwargs["params"]["Fields"] \ No newline at end of file diff --git a/src/apiary/tests/test_twilio/test_unit_twilio.py b/src/apiary/tests/test_twilio/test_unit_twilio.py new file mode 100644 index 0000000..b62107c --- /dev/null +++ b/src/apiary/tests/test_twilio/test_unit_twilio.py @@ -0,0 +1,45 @@ +import httpx +import pytest +from unittest.mock import patch, MagicMock +from apiary.api_connectors.twilio import TwilioConnector + + +def test_init_with_all_keys(): + connector = TwilioConnector(api_sid="sid", api_secret="secret") + assert connector.api_sid == "sid" + assert connector.auth is not None + + +@patch.dict("os.environ", {"TWILIO_API_SID": "sid", "TWILIO_API_SECRET": "secret"}, clear=True) +def test_init_with_env_keys(): + connector = TwilioConnector(load_env_vars=True) + assert connector.api_sid == "sid" + assert connector.auth is not None + + +@patch("apiary.api_connectors.broker.combine_env_configs", return_value={}) +def test_init_missing_auth_keys(mock_env): + with pytest.raises(ValueError, match="TWILIO_API_SID and TWILIO_API_SECRET are required"): + TwilioConnector(load_env_vars=True) + + +@patch("apiary.api_connectors.twilio.TwilioConnector._make_request") +def test_lookup_phone(mock_request): + import json + # Return a real httpx.Response to reflect new return type + req = httpx.Request("GET", "https://lookups.twilio.com/v2/PhoneNumbers/15555555555") + payload = {"carrier": "example"} + resp = httpx.Response( + 200, + request=req, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_request.return_value = resp + + connector = TwilioConnector(api_sid="sid", api_secret="secret") + result = connector.lookup_phone("15555555555") + + assert isinstance(result, httpx.Response) + assert result.json() == payload + mock_request.assert_called_once() \ No newline at end of file diff --git a/src/apiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml b/src/apiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml new file mode 100644 index 0000000..8a8d716 --- /dev/null +++ b/src/apiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml @@ -0,0 +1,279 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + api-key: + - REDACTED + connection: + - keep-alive + host: + - urlscan.io + user-agent: + - REDACTED + method: GET + uri: https://urlscan.io/api/v1/result/01958568-c986-7001-816d-9e0ccd7c4c4a + response: + body: + string: !!binary | + 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 + headers: + Cache-Control: + - private, max-age=60 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - 'default-src ''self'' data: urlscan.io sentry.urlscan.io; script-src ''self'' + data: developers.google.com www.google.com www.gstatic.com urlscan.io sentry.urlscan.io; + style-src ''self'' fonts.googleapis.com www.google.com cdnjs.cloudflare.com + urlscan.io; img-src * data:; font-src ''self'' fonts.gstatic.com cdnjs.cloudflare.com + urlscan.io; child-src ''self''; frame-src https://www.google.com/recaptcha/; + form-action ''self''; connect-src ''self'' sentry.urlscan.io urlscan.io; upgrade-insecure-requests; + frame-ancestors ''none'';' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 11 Jul 2025 18:21:17 GMT + ETag: + - W/"13300-LhkzgtpQ7gn0AN/U3Gp0k02sIZs" + Referrer-Policy: + - same-origin + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Proxy-Cache: + - MISS + X-Rate-Limit-Action: + - retrieve + X-Rate-Limit-Limit: + - '2500' + X-Rate-Limit-Remaining: + - '2499' + X-Rate-Limit-Reset: + - '2025-07-11T18:22:00.000Z' + X-Rate-Limit-Reset-After: + - '42' + X-Rate-Limit-Scope: + - team + X-Rate-Limit-Window: + - minute + X-Robots-Tag: + - all + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/src/apiary/tests/test_urlscan/test_integration_urlscan.py b/src/apiary/tests/test_urlscan/test_integration_urlscan.py new file mode 100644 index 0000000..fdae573 --- /dev/null +++ b/src/apiary/tests/test_urlscan/test_integration_urlscan.py @@ -0,0 +1,12 @@ +import httpx +import pytest +from apiary.api_connectors.urlscan import URLScanConnector + +@pytest.mark.integration +def test_urlscan_results_vcr(vcr_cassette): + with vcr_cassette.use_cassette("test_urlscan_results_vcr"): + connector = URLScanConnector(load_env_vars=True, enable_logging=True) + result = connector.results("01958568-c986-7001-816d-9e0ccd7c4c4a") + + assert isinstance(result, httpx.Response) + assert "task" in result.json() \ No newline at end of file diff --git a/src/apiary/tests/test_urlscan/test_unit_async_urlscan.py b/src/apiary/tests/test_urlscan/test_unit_async_urlscan.py new file mode 100644 index 0000000..58a2a58 --- /dev/null +++ b/src/apiary/tests/test_urlscan/test_unit_async_urlscan.py @@ -0,0 +1,49 @@ +import pytest +from httpx import Response +from httpx import Request +from apiary.api_connectors.urlscan import AsyncURLScanConnector + +@pytest.mark.asyncio +async def test_async_init_with_api_key(): + connector = AsyncURLScanConnector(api_key="testkey") + assert connector.api_key == "testkey" + assert connector.headers["API-Key"] == "testkey" + +@pytest.mark.asyncio +async def test_async_init_with_env(monkeypatch): + monkeypatch.setenv("URLSCAN_API_KEY", "envkey") + connector = AsyncURLScanConnector(load_env_vars=True) + assert connector.api_key == "envkey" + assert connector.headers["API-Key"] == "envkey" + +@pytest.mark.asyncio +async def test_async_init_missing_key_raises(monkeypatch): + monkeypatch.delenv("URLSCAN_API_KEY", raising=False) + with pytest.raises(ValueError, match="API key is required for AsyncURLScanConnector"): + AsyncURLScanConnector() + +@pytest.mark.asyncio +async def test_async_search_makes_expected_call(monkeypatch): + connector = AsyncURLScanConnector(api_key="testkey") + + async def mock_get(path, params=None): + assert path == "/api/v1/search/" + assert params["q"] == "example.com" + return Response(200, request=Request("GET", path)) + + connector.get = mock_get + resp = await connector.search("example.com") + assert resp.status_code == 200 + +@pytest.mark.asyncio +async def test_async_scan_makes_expected_call(monkeypatch): + connector = AsyncURLScanConnector(api_key="testkey") + + async def mock_post(path, json=None): + assert path == "/api/v1/scan" + assert json["url"] == "http://example.com" + return Response(200, request=Request("POST", path)) + + connector.post = mock_post + resp = await connector.scan("http://example.com") + assert resp.status_code == 200 \ No newline at end of file diff --git a/src/apiary/tests/test_urlscan/test_unit_urlscan.py b/src/apiary/tests/test_urlscan/test_unit_urlscan.py new file mode 100644 index 0000000..9c9916d --- /dev/null +++ b/src/apiary/tests/test_urlscan/test_unit_urlscan.py @@ -0,0 +1,39 @@ +import httpx +import pytest +from unittest.mock import patch, MagicMock +from apiary.api_connectors.urlscan import URLScanConnector + +def test_init_with_api_key(): + connector = URLScanConnector(api_key="test_key") + assert connector.api_key == "test_key" + +@patch.dict("os.environ", {"URLSCAN_API_KEY": "env_key"}, clear=True) +def test_init_with_env_key(): + connector = URLScanConnector(load_env_vars=True) + assert connector.api_key == "env_key" + +@patch("apiary.api_connectors.broker.combine_env_configs", return_value={}) +def test_init_missing_auth_keys(mock_env): + with pytest.raises(ValueError, match="API key is required for URLScanConnector"): + URLScanConnector(load_env_vars=True) + +@patch("apiary.api_connectors.urlscan.URLScanConnector.get") +def test_results(mock_get): + import json + + # Build a real httpx.Response to match the new return type + request = httpx.Request("GET", "https://urlscan.io/api/v1/result/abc123") + payload = {"task": "test"} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_get.return_value = mock_response + + connector = URLScanConnector(api_key="test_key") + result = connector.results("abc123") + + assert isinstance(result, httpx.Response) + assert result.json() == payload \ No newline at end of file From 42d399a3a3436a0b2a9bafa3e3bbeb476512e71c Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:39:08 -0500 Subject: [PATCH 5/9] update dev env test files --- dev_env/api/test_flashpoint.py | 2 +- dev_env/api/test_urlscan.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_env/api/test_flashpoint.py b/dev_env/api/test_flashpoint.py index 32d63ed..c44a958 100644 --- a/dev_env/api/test_flashpoint.py +++ b/dev_env/api/test_flashpoint.py @@ -1,4 +1,4 @@ -from ppp_connectors.api_connectors.flashpoint import FlashpointConnector +from apiary.api_connectors.flashpoint import FlashpointConnector import httpx fp = FlashpointConnector( diff --git a/dev_env/api/test_urlscan.py b/dev_env/api/test_urlscan.py index 16eca5b..35ee462 100644 --- a/dev_env/api/test_urlscan.py +++ b/dev_env/api/test_urlscan.py @@ -1,4 +1,4 @@ -from ppp_connectors.api_connectors.urlscan import URLScanConnector, AsyncURLScanConnector +from apiary.api_connectors.urlscan import URLScanConnector, AsyncURLScanConnector urlscan = URLScanConnector( load_env_vars=True, From c277d7b335360bab6c91630285f14488f49a8ef4 Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:40:17 -0500 Subject: [PATCH 6/9] add local dev environment test scripts --- dev_env/elasticsearch/test_es.py | 4 ++-- dev_env/mongo/test_mongo.py | 4 ++-- dev_env/mongo/test_mongo_async.py | 4 ++-- dev_env/odbc/test_odbc.py | 4 ++-- dev_env/splunk/test_splunk.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev_env/elasticsearch/test_es.py b/dev_env/elasticsearch/test_es.py index 2b38390..2854300 100644 --- a/dev_env/elasticsearch/test_es.py +++ b/dev_env/elasticsearch/test_es.py @@ -1,5 +1,5 @@ -from ppp_connectors.dbms_connectors.elasticsearch import ElasticsearchConnector -from ppp_connectors.helpers import combine_env_configs, setup_logger +from apiary.dbms_connectors.elasticsearch import ElasticsearchConnector +from apiary.helpers import combine_env_configs, setup_logger from typing import Dict, Any import json diff --git a/dev_env/mongo/test_mongo.py b/dev_env/mongo/test_mongo.py index cdbaa81..9e6a13f 100644 --- a/dev_env/mongo/test_mongo.py +++ b/dev_env/mongo/test_mongo.py @@ -1,6 +1,6 @@ from typing import Dict, Any, List -from ppp_connectors.dbms_connectors.mongo import MongoConnector -from ppp_connectors.helpers import combine_env_configs, setup_logger +from apiary.dbms_connectors.mongo import MongoConnector +from apiary.helpers import combine_env_configs, setup_logger def main() -> None: diff --git a/dev_env/mongo/test_mongo_async.py b/dev_env/mongo/test_mongo_async.py index bcf853d..80c18c4 100644 --- a/dev_env/mongo/test_mongo_async.py +++ b/dev_env/mongo/test_mongo_async.py @@ -1,8 +1,8 @@ import asyncio from typing import Any, Dict, List -from ppp_connectors.dbms_connectors.mongo_async import AsyncMongoConnector -from ppp_connectors.helpers import combine_env_configs, setup_logger +from apiary.dbms_connectors.mongo_async import AsyncMongoConnector +from apiary.helpers import combine_env_configs, setup_logger async def main() -> None: diff --git a/dev_env/odbc/test_odbc.py b/dev_env/odbc/test_odbc.py index f89e2cb..7bf4589 100644 --- a/dev_env/odbc/test_odbc.py +++ b/dev_env/odbc/test_odbc.py @@ -1,5 +1,5 @@ -from ppp_connectors.dbms_connectors.odbc import ODBCConnector -from ppp_connectors.helpers import combine_env_configs, setup_logger +from apiary.dbms_connectors.odbc import ODBCConnector +from apiary.helpers import combine_env_configs, setup_logger from typing import Dict, Any env_config: Dict[str, Any] = combine_env_configs() diff --git a/dev_env/splunk/test_splunk.py b/dev_env/splunk/test_splunk.py index 8d888df..84c8916 100644 --- a/dev_env/splunk/test_splunk.py +++ b/dev_env/splunk/test_splunk.py @@ -1,5 +1,5 @@ -from ppp_connectors.dbms_connectors.splunk import SplunkConnector -from ppp_connectors.helpers import combine_env_configs, setup_logger +from apiary.dbms_connectors.splunk import SplunkConnector +from apiary.helpers import combine_env_configs, setup_logger from typing import Dict, Any env_config: Dict[str, Any] = combine_env_configs() From 1ca12e00f53658670037a0550fd7dc2f09724d68 Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:48:12 -0500 Subject: [PATCH 7/9] update poetry lock --- poetry.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index c02077d..3326af1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -682,6 +682,7 @@ description = "DB API module for ODBC" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"odbc\"" files = [ {file = "pyodbc-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5"}, {file = "pyodbc-5.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2"}, @@ -1372,7 +1373,7 @@ multidict = ">=4.0" propcache = ">=0.2.1" [extras] -odbc = [] +odbc = ["pyodbc"] [metadata] lock-version = "2.1" From f71b6a99bb4645183139e41adee77a20d392932b Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 10:57:07 -0500 Subject: [PATCH 8/9] attempting to fix pythonpath for ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b71f550..6c50841 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,4 +35,5 @@ jobs: TWILIO_API_SID: dummy TWILIO_API_SECRET: dummy URLSCAN_API_KEY: dummy + PYTHONPATH: . run: poetry run pytest --ignore=dev_env \ No newline at end of file From f4b79b6e022144c2dbaf6a8b0c3c4db44f2c17f9 Mon Sep 17 00:00:00 2001 From: Rob D'Aveta Date: Tue, 16 Dec 2025 11:09:10 -0500 Subject: [PATCH 9/9] Remove legacy ppp_connectors package and test suite --- dev_env/api/test_async_generic.py | 57 + dev_env/api/test_ipqs.py | 28 + dev_env/example.py | 4 +- ppp_connectors/__init__.py | 43 - ppp_connectors/api_connectors/__init__.py | 0 ppp_connectors/api_connectors/broker.py | 398 ---- ppp_connectors/api_connectors/flashpoint.py | 195 -- ppp_connectors/api_connectors/generic.py | 105 - ppp_connectors/api_connectors/ipqs.py | 68 - ppp_connectors/api_connectors/spycloud.py | 207 -- ppp_connectors/api_connectors/twilio.py | 114 - ppp_connectors/api_connectors/urlscan.py | 148 -- ppp_connectors/broker.py | 14 - ppp_connectors/dbms_connectors/__init__.py | 0 .../dbms_connectors/elasticsearch.py | 143 -- ppp_connectors/dbms_connectors/mongo.py | 390 ---- ppp_connectors/dbms_connectors/mongo_async.py | 323 --- ppp_connectors/dbms_connectors/odbc.py | 110 - ppp_connectors/dbms_connectors/splunk.py | 131 -- ppp_connectors/helpers.py | 102 - ppp_connectors/tests/__init__.py | 0 ppp_connectors/tests/conftest.py | 47 - .../test_broker/test_integration_broker.py | 14 - .../test_broker/test_unit_asyncbroker.py | 17 - .../tests/test_broker/test_unit_broker.py | 67 - .../test_unit_elasticsearch.py | 67 - .../test_flashpoint_search_fraud_vcr.yaml | 66 - .../test_integration_flashpoint.py | 11 - .../test_unit_async_flashpoint.py | 49 - .../test_flashpoint/test_unit_flashpoint.py | 45 - .../test_generic_get_github_api.yaml | 87 - .../test_integration_generic_connector.py | 12 - .../test_unit_async_generic_connector.py | 54 - .../test_unit_generic_connector.py | 34 - ppp_connectors/tests/test_ipqs/__init__.py | 0 .../test_ipqs_malicious_url_vcr.yaml | 64 - .../tests/test_ipqs/test_integration_ipqs.py | 13 - .../tests/test_ipqs/test_unit_async_ipqs.py | 53 - .../tests/test_ipqs/test_unit_ipqs.py | 45 - .../test_mongodb/test_unit_async_mongo.py | 109 - .../tests/test_mongodb/test_unit_mongo.py | 219 -- .../tests/test_odbc/test_unit_odbc.py | 82 - .../tests/test_splunk/test_unit_splunk.py | 56 - .../test_spycloud_ato_search_vcr.yaml | 1870 ----------------- .../test_integration_spycloud.py | 12 - .../test_spycloud/test_unit_async_spycloud.py | 44 - .../tests/test_spycloud/test_unit_spycloud.py | 46 - .../cassettes/test_lookup_phone_vcr.yaml | 68 - .../test_twilio/test_integration_twilio.py | 14 - .../test_twilio/test_unit_async_twilio.py | 34 - .../tests/test_twilio/test_unit_twilio.py | 45 - .../cassettes/test_urlscan_results_vcr.yaml | 279 --- .../test_urlscan/test_integration_urlscan.py | 12 - .../test_urlscan/test_unit_async_urlscan.py | 49 - .../tests/test_urlscan/test_unit_urlscan.py | 39 - 55 files changed, 87 insertions(+), 6216 deletions(-) create mode 100644 dev_env/api/test_async_generic.py create mode 100644 dev_env/api/test_ipqs.py delete mode 100644 ppp_connectors/__init__.py delete mode 100644 ppp_connectors/api_connectors/__init__.py delete mode 100644 ppp_connectors/api_connectors/broker.py delete mode 100644 ppp_connectors/api_connectors/flashpoint.py delete mode 100644 ppp_connectors/api_connectors/generic.py delete mode 100644 ppp_connectors/api_connectors/ipqs.py delete mode 100644 ppp_connectors/api_connectors/spycloud.py delete mode 100644 ppp_connectors/api_connectors/twilio.py delete mode 100644 ppp_connectors/api_connectors/urlscan.py delete mode 100644 ppp_connectors/broker.py delete mode 100644 ppp_connectors/dbms_connectors/__init__.py delete mode 100644 ppp_connectors/dbms_connectors/elasticsearch.py delete mode 100644 ppp_connectors/dbms_connectors/mongo.py delete mode 100644 ppp_connectors/dbms_connectors/mongo_async.py delete mode 100644 ppp_connectors/dbms_connectors/odbc.py delete mode 100644 ppp_connectors/dbms_connectors/splunk.py delete mode 100644 ppp_connectors/helpers.py delete mode 100644 ppp_connectors/tests/__init__.py delete mode 100644 ppp_connectors/tests/conftest.py delete mode 100644 ppp_connectors/tests/test_broker/test_integration_broker.py delete mode 100644 ppp_connectors/tests/test_broker/test_unit_asyncbroker.py delete mode 100644 ppp_connectors/tests/test_broker/test_unit_broker.py delete mode 100644 ppp_connectors/tests/test_elasticsearch/test_unit_elasticsearch.py delete mode 100644 ppp_connectors/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml delete mode 100644 ppp_connectors/tests/test_flashpoint/test_integration_flashpoint.py delete mode 100644 ppp_connectors/tests/test_flashpoint/test_unit_async_flashpoint.py delete mode 100644 ppp_connectors/tests/test_flashpoint/test_unit_flashpoint.py delete mode 100644 ppp_connectors/tests/test_generic/cassettes/test_generic_get_github_api.yaml delete mode 100644 ppp_connectors/tests/test_generic/test_integration_generic_connector.py delete mode 100644 ppp_connectors/tests/test_generic/test_unit_async_generic_connector.py delete mode 100644 ppp_connectors/tests/test_generic/test_unit_generic_connector.py delete mode 100644 ppp_connectors/tests/test_ipqs/__init__.py delete mode 100644 ppp_connectors/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml delete mode 100644 ppp_connectors/tests/test_ipqs/test_integration_ipqs.py delete mode 100644 ppp_connectors/tests/test_ipqs/test_unit_async_ipqs.py delete mode 100644 ppp_connectors/tests/test_ipqs/test_unit_ipqs.py delete mode 100644 ppp_connectors/tests/test_mongodb/test_unit_async_mongo.py delete mode 100644 ppp_connectors/tests/test_mongodb/test_unit_mongo.py delete mode 100644 ppp_connectors/tests/test_odbc/test_unit_odbc.py delete mode 100644 ppp_connectors/tests/test_splunk/test_unit_splunk.py delete mode 100644 ppp_connectors/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml delete mode 100644 ppp_connectors/tests/test_spycloud/test_integration_spycloud.py delete mode 100644 ppp_connectors/tests/test_spycloud/test_unit_async_spycloud.py delete mode 100644 ppp_connectors/tests/test_spycloud/test_unit_spycloud.py delete mode 100644 ppp_connectors/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml delete mode 100644 ppp_connectors/tests/test_twilio/test_integration_twilio.py delete mode 100644 ppp_connectors/tests/test_twilio/test_unit_async_twilio.py delete mode 100644 ppp_connectors/tests/test_twilio/test_unit_twilio.py delete mode 100644 ppp_connectors/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml delete mode 100644 ppp_connectors/tests/test_urlscan/test_integration_urlscan.py delete mode 100644 ppp_connectors/tests/test_urlscan/test_unit_async_urlscan.py delete mode 100644 ppp_connectors/tests/test_urlscan/test_unit_urlscan.py diff --git a/dev_env/api/test_async_generic.py b/dev_env/api/test_async_generic.py new file mode 100644 index 0000000..db3951f --- /dev/null +++ b/dev_env/api/test_async_generic.py @@ -0,0 +1,57 @@ +import asyncio +from typing import Any, Dict, Iterable, Tuple + +from apiary.api_connectors.generic import AsyncGenericConnector + +# Give AsyncGenericConnector lightweight context-manager support without +# modifying the library source. +if not hasattr(AsyncGenericConnector, "__aenter__"): + async def _async_enter(self: AsyncGenericConnector) -> AsyncGenericConnector: + return self + + async def _async_exit( + self: AsyncGenericConnector, + exc_type, + exc, + tb, + ) -> None: + await self.session.aclose() + + AsyncGenericConnector.__aenter__ = _async_enter # type: ignore[attr-defined] + AsyncGenericConnector.__aexit__ = _async_exit # type: ignore[attr-defined] + +RequestSpec = Tuple[str, str, Dict[str, Any]] + +REQUESTS: Iterable[RequestSpec] = ( + ("GET", "/get", {"params": {"label": "alpha"}}), + ("GET", "/delay/1", {"params": {"label": "beta"}}), + ("GET", "/uuid", {"headers": {"X-Demo": "gamma"}}), +) + + +async def fetch(connector: AsyncGenericConnector, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: + response = await connector.request(method, endpoint, **kwargs) + return { + "endpoint": endpoint, + "status": response.status_code, + "json": response.json(), + } + + +async def main() -> None: + async with AsyncGenericConnector( + base_url="http://httpbin.org", + enable_logging=False, + timeout=15, + ) as connector: + tasks = [fetch(connector, method, endpoint, **req_kwargs) for method, endpoint, req_kwargs in REQUESTS] + results = await asyncio.gather(*tasks) + + print("Completed concurrent requests:") + for result in results: + print(f"- {result['endpoint']} -> {result['status']}") + print(f" payload: {result['json']}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/dev_env/api/test_ipqs.py b/dev_env/api/test_ipqs.py new file mode 100644 index 0000000..dfac88a --- /dev/null +++ b/dev_env/api/test_ipqs.py @@ -0,0 +1,28 @@ +from apiary.api_connectors.ipqs import AsyncIPQSConnector, IPQSConnector +import asyncio +from apiary.helpers import combine_env_configs, setup_logger +from typing import Dict, Any +import json + + +env_config: Dict[str, Any] = combine_env_configs() + +async def main(): + ipqs = AsyncIPQSConnector( + api_key=env_config["IPQS_API_KEY"], + enable_logging=True, + load_env_vars=True, + trust_env=True, + verify=False + ) + + res = await ipqs.malicious_url( + query="cracked.to" + ) + + print(res) + print(res.json()) + print(res.headers) + +asyncio.run(main()) +# main() \ No newline at end of file diff --git a/dev_env/example.py b/dev_env/example.py index fbd1f51..e7df5df 100644 --- a/dev_env/example.py +++ b/dev_env/example.py @@ -1,5 +1,5 @@ -from ppp_connectors.dbms_connectors.mongo import MongoConnector -from ppp_connectors.helpers import combine_env_configs +from apiary.dbms_connectors.mongo import MongoConnector +from apiary.helpers import combine_env_configs from typing import Dict, Any env_config: Dict[str, Any] = combine_env_configs() diff --git a/ppp_connectors/__init__.py b/ppp_connectors/__init__.py deleted file mode 100644 index 5872ab9..0000000 --- a/ppp_connectors/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# API connectors -import warnings - -warnings.warn( - ( - "The 'ppp_connectors' package is deprecated and no longer maintained. " - "Please migrate to the 'apiary' package. " - "See https://github.com/robd518/apiary for details." - ), - DeprecationWarning, - stacklevel=2, -) - -from ppp_connectors.api_connectors import ( - urlscan, - spycloud, - twilio, - flashpoint, - ipqs, - generic, -) - -# DBMS connectors -from ppp_connectors.dbms_connectors import ( - elasticsearch, - mongo, - odbc, - splunk -) - -# Export the modules and re-exports -__all__ = [ - "elasticsearch", - "flashpoint", - "generic", - "ipqs", - "mongo", - "odbc", - "splunk", - "spycloud", - "twilio", - "urlscan", -] \ No newline at end of file diff --git a/ppp_connectors/api_connectors/__init__.py b/ppp_connectors/api_connectors/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ppp_connectors/api_connectors/broker.py b/ppp_connectors/api_connectors/broker.py deleted file mode 100644 index 571f87f..0000000 --- a/ppp_connectors/api_connectors/broker.py +++ /dev/null @@ -1,398 +0,0 @@ -import httpx -from httpx import Auth -from typing import Optional, Dict, Any, Union, Iterable, Callable, ParamSpec, TypeVar, Type -from tenacity import retry, stop_after_attempt, wait_exponential, RetryError, retry_if_exception, AsyncRetrying -from ppp_connectors.helpers import setup_logger, combine_env_configs -from functools import wraps -import inspect -import os -from types import TracebackType - - -P = ParamSpec("P") -R = TypeVar("R") - -def log_method_call(func: Callable[P, R]) -> Callable[P, R]: - @wraps(func) - def wrapper(self, *args, **kwargs): - caller = func.__name__ - sig = inspect.signature(func) - bound = sig.bind(self, *args, **kwargs) - bound.apply_defaults() - query_value = bound.arguments.get("query") - self._log(f"{caller} called with query: {query_value}") - return func(self, *args, **kwargs) - return wrapper - - -def bubble_broker_init_signature(*, exclude: Iterable[str] = ("base_url",)): - """ - Class decorator that augments a connector subclass' __init__ signature with - parameters from Broker.__init__ for better IDE/tab-completion hints. - - Usage: - from ppp_connectors.api_connectors.broker import Broker, bubble_broker_init_signature - - @bubble_broker_init_signature() - class MyConnector(Broker): - def __init__(self, api_key: str | None = None, **kwargs): - super().__init__(base_url="https://example.com", **kwargs) - ... - - Notes: - - This affects *introspection only* (via __signature__). Runtime behavior is unchanged. - - Subclass-specific parameters remain first (e.g., api_key), followed by Broker params. - - `base_url` is excluded by default since subclasses set it themselves. - - The subclass' **kwargs (if present) is preserved at the end so httpx.Client kwargs - can still be passed through. - """ - def _decorate(cls): - sub_init = cls.__init__ - broker_init = Broker.__init__ - - sub_sig = inspect.signature(sub_init) - broker_sig = inspect.signature(broker_init) - - new_params = [] - saw_var_kw = None - - # Keep subclass params first; remember its **kwargs if present - for p in sub_sig.parameters.values(): - if p.kind is inspect.Parameter.VAR_KEYWORD: - saw_var_kw = p - else: - new_params.append(p) - - present = {p.name for p in new_params} - - # Append Broker params (skip self, excluded, already-present, and **kwargs) - for name, p in list(broker_sig.parameters.items())[1:]: - if name in exclude or name in present: - continue - if p.kind is inspect.Parameter.VAR_KEYWORD: - continue - new_params.append(p) - - # Re-append subclass **kwargs (or add a generic one to keep flexibility) - if saw_var_kw is not None: - new_params.append(saw_var_kw) - else: - new_params.append( - inspect.Parameter( - "client_kwargs", - kind=inspect.Parameter.VAR_KEYWORD, - ) - ) - - cls.__init__.__signature__ = inspect.Signature(parameters=new_params) - return cls - - return _decorate - - -class SharedConnectorBase: - """ - Shared base class for Broker and AsyncBroker. - Houses reusable logic (constructor, logging, proxy config, retry predicate). - """ - def __init__( - self, - base_url: str, - headers: Optional[Dict[str, str]] = None, - enable_logging: bool = False, - enable_backoff: bool = False, - timeout: int = 10, - load_env_vars: bool = False, - trust_env: bool = True, - proxy: Optional[str] = None, - mounts: Optional[Dict[str, httpx.HTTPTransport]] = None, - **client_kwargs, - ): - self.base_url = base_url.rstrip('/') - self.logger = setup_logger(self.__class__.__name__) if enable_logging else None - self.enable_backoff = enable_backoff - self.timeout = timeout - self.headers = headers or {} - self.trust_env = trust_env - self.proxy = proxy - self.mounts = mounts - self.env_config = combine_env_configs() if load_env_vars else {} - self._client_kwargs = dict(client_kwargs) if client_kwargs else {} - - def _log(self, message: str): - if self.logger: - self.logger.info(message) - - def _collect_proxy_config(self) -> tuple[Optional[str], Optional[Dict[str, httpx.HTTPTransport]]]: - source_env: Optional[Dict[str, str]] = None - if isinstance(self.env_config, dict) and len(self.env_config) > 0: - source_env = {k: v for k, v in self.env_config.items() if isinstance(k, str) and isinstance(v, str)} - elif self.trust_env: - source_env = dict(os.environ) - else: - return None, None - - def _get(key: str) -> Optional[str]: - return source_env.get(key) or source_env.get(key.lower()) - - all_proxy = _get("ALL_PROXY") - http_proxy = _get("HTTP_PROXY") - https_proxy = _get("HTTPS_PROXY") - - if http_proxy and https_proxy and http_proxy != https_proxy: - return None, { - "http://": httpx.HTTPTransport(proxy=http_proxy), - "https://": httpx.HTTPTransport(proxy=https_proxy), - } - single = all_proxy or https_proxy or http_proxy - if single: - return single, None - return None, None - - @staticmethod - def _default_retry_exc(exc: BaseException) -> bool: - if isinstance(exc, httpx.HTTPStatusError): - r = exc.response - if r is not None: - return r.status_code == 429 or 500 <= r.status_code < 600 - return isinstance(exc, ( - httpx.ConnectError, - httpx.ReadTimeout, - httpx.WriteError, - httpx.RemoteProtocolError, - httpx.PoolTimeout, - )) - - -class Broker(SharedConnectorBase): - """ - A base HTTP client that provides structured request handling, logging, retries, and optional environment config loading. - Designed to be inherited by specific API connector classes. - """ - def __init__( - self, - base_url: str, - headers: Optional[Dict[str, str]] = None, - enable_logging: bool = False, - enable_backoff: bool = False, - timeout: int = 10, - load_env_vars: bool = False, - trust_env: bool = True, - proxy: Optional[str] = None, - mounts: Optional[Dict[str, httpx.HTTPTransport]] = None, - **client_kwargs, - ): - super().__init__( - base_url=base_url, - headers=headers, - enable_logging=enable_logging, - enable_backoff=enable_backoff, - timeout=timeout, - load_env_vars=load_env_vars, - trust_env=trust_env, - proxy=proxy, - mounts=mounts, - **client_kwargs, - ) - - client_options = dict(self._client_kwargs) - client_options.pop("timeout", None) - - client_args = { - "timeout": self.timeout, - "trust_env": self.trust_env, - **client_options, - } - - if self.mounts: - client_args["mounts"] = self.mounts - elif self.proxy: - client_args["proxy"] = self.proxy - else: - env_proxy, env_mounts = self._collect_proxy_config() - if env_mounts: - self.mounts = env_mounts - client_args["mounts"] = self.mounts - elif env_proxy: - self.proxy = env_proxy - client_args["proxy"] = self.proxy - elif not self.trust_env: - client_args["trust_env"] = False - - self.session = httpx.Client(**client_args) - - def __enter__(self) -> "Broker": - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc: Optional[BaseException], - tb: Optional[TracebackType], - ) -> None: - self.session.close() - - def _make_request( - self, - method: str, - endpoint: str, - params: Optional[Dict[str, Any]] = None, - json: Optional[Dict[str, Any]] = None, - auth: Optional[Union[tuple, Auth]] = None, - headers: Optional[Dict[str, str]] = None, - retry_kwargs: Optional[Dict[str, Any]] = None, - **request_kwargs, - ) -> httpx.Response: - url = f"{self.base_url}/{endpoint.lstrip('/')}" - - def do_request() -> httpx.Response: - resp = self.session.request( - method=method, - url=url, - headers=headers or self.headers, - params=params, - json=json, - auth=auth, - **request_kwargs, - ) - resp.raise_for_status() - return resp - - call = do_request - if self.enable_backoff: - rk = dict(retry_kwargs or {}) - if "retry" not in rk: - rk["retry"] = retry_if_exception(self._default_retry_exc) - if "stop" not in rk: - rk["stop"] = stop_after_attempt(3) - if "wait" not in rk: - rk["wait"] = wait_exponential(multiplier=1, min=2, max=10) - call = retry(reraise=True, **rk)(do_request) - - try: - return call() - except RetryError as re: - last = re.last_attempt.exception() - self._log(f"Retry failed: {last}") - raise - except httpx.HTTPStatusError as he: - self._log(f"HTTP error: {he}") - raise - - def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: - return self._make_request("GET", endpoint, params=params, **kwargs) - - def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: - return self._make_request("POST", endpoint, json=json, **kwargs) - - -class AsyncBroker(SharedConnectorBase): - """ - Async HTTP client connector. Provides async _make_request, get, and post. - """ - def __init__( - self, - base_url: str, - headers: Optional[Dict[str, str]] = None, - enable_logging: bool = False, - enable_backoff: bool = False, - timeout: int = 10, - load_env_vars: bool = False, - trust_env: bool = True, - proxy: Optional[str] = None, - mounts: Optional[Dict[str, httpx.HTTPTransport]] = None, - **client_kwargs, - ): - super().__init__( - base_url=base_url, - headers=headers, - enable_logging=enable_logging, - enable_backoff=enable_backoff, - timeout=timeout, - load_env_vars=load_env_vars, - trust_env=trust_env, - proxy=proxy, - mounts=mounts, - **client_kwargs, - ) - - if self.mounts: - raise ValueError("The 'mounts' parameter is not supported in AsyncBroker but " - "you can still use 'proxy' or 'trust_env' if 'HTTP_PROXY' or " - "'HTTPS_PROXY' are in your system environment variables ") - - resolved_proxy = self.proxy or self._collect_proxy_config()[0] - - self.session = httpx.AsyncClient( - timeout=self.timeout, - proxy=resolved_proxy, - trust_env=self.trust_env, - **self._client_kwargs, - ) - - async def __aenter__(self) -> "AsyncBroker": - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc: Optional[BaseException], - tb: Optional[TracebackType], - ) -> None: - await self.session.aclose() - - async def _make_request( - self, - method: str, - endpoint: str, - params: Optional[Dict[str, Any]] = None, - json: Optional[Dict[str, Any]] = None, - auth: Optional[Union[tuple, Auth]] = None, - headers: Optional[Dict[str, str]] = None, - retry_kwargs: Optional[Dict[str, Any]] = None, - **request_kwargs, - ) -> httpx.Response: - url = f"{self.base_url}/{endpoint.lstrip('/')}" - - async def do_request() -> httpx.Response: - resp = await self.session.request( - method=method, - url=url, - headers=headers or self.headers, - params=params, - json=json, - auth=auth, - **request_kwargs, - ) - resp.raise_for_status() - return resp - - call = do_request - if self.enable_backoff: - - rk = dict(retry_kwargs or {}) - retry_pred = rk.get("retry", retry_if_exception(self._default_retry_exc)) - stop_cond = rk.get("stop", stop_after_attempt(3)) - wait_cond = rk.get("wait", wait_exponential(multiplier=1, min=2, max=10)) - - async def retry_wrapper(): - async for attempt in AsyncRetrying(reraise=True, retry=retry_pred, stop=stop_cond, wait=wait_cond): - with attempt: - return await do_request() - call = retry_wrapper - - try: - return await call() - except RetryError as re: - last = re.last_attempt.exception() - self._log(f"Retry failed: {last}") - raise - except httpx.HTTPStatusError as he: - self._log(f"HTTP error: {he}") - raise - - async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: - return await self._make_request("GET", endpoint, params=params, **kwargs) - - async def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response: - return await self._make_request("POST", endpoint, json=json, **kwargs) diff --git a/ppp_connectors/api_connectors/flashpoint.py b/ppp_connectors/api_connectors/flashpoint.py deleted file mode 100644 index 40b1bd1..0000000 --- a/ppp_connectors/api_connectors/flashpoint.py +++ /dev/null @@ -1,195 +0,0 @@ -import httpx -from typing import Optional -from ppp_connectors.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call - -@bubble_broker_init_signature() -class FlashpointConnector(Broker): - """ - FlashpointConnector provides access to various Flashpoint API search and retrieval endpoints - using a consistent Broker-based interface. - - Attributes: - api_key (str): Flashpoint API token used for bearer authentication. - """ - def __init__(self, api_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://api.flashpoint.io", **kwargs) - self.api_key = api_key or self.env_config.get("FLASHPOINT_API_KEY") - if not self.api_key: - raise ValueError("FLASHPOINT_API_KEY is required") - self.headers.update({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": f"Bearer {self.api_key}", - }) - - @log_method_call - def search_communities(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint communities data. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return self.post("/sources/v2/communities", json={"query": query, **kwargs}) - - @log_method_call - def search_fraud(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint fraud datasets. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return self.post("/sources/v2/fraud", json={"query": query, **kwargs}) - - @log_method_call - def search_marketplaces(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint marketplace datasets. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return self.post("/sources/v2/markets", json={"query": query, **kwargs}) - - @log_method_call - def search_media(self, query: str, **kwargs) -> httpx.Response: - """ - Search OCR-processed media from Flashpoint. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return self.post("/sources/v2/media", json={"query": query, **kwargs}) - - @log_method_call - def get_media_object(self, query: str, **kwargs) -> httpx.Response: - """ - Retrieve metadata for a specific media object. - - Args: - query (str): The media_id of the object to retrieve. - **kwargs: Additional request options. - """ - return self.get(f"/sources/v2/media/{query}") - - @log_method_call - def get_media_image(self, query: str, **kwargs) -> httpx.Response: - """ - Download image asset by storage_uri. - - Args: - query (str): The storage_uri (asset_id) of the image to download. - **kwargs: Additional request options. - """ - safe_headers = {"Authorization": f"Bearer {self.api_key}"} - return self.get("/sources/v1/media/", headers=safe_headers, params={"asset_id": query}) - - @log_method_call - def search_checks(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint fraud check datasets. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return self.post("/sources/v2/fraud/checks", json={"query": query, **kwargs}) - - -# Async version of FlashpointConnector -@bubble_broker_init_signature() -class AsyncFlashpointConnector(AsyncBroker): - """ - AsyncFlashpointConnector provides async access to Flashpoint API endpoints. - """ - def __init__(self, api_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://api.flashpoint.io", **kwargs) - self.api_key = api_key or self.env_config.get("FLASHPOINT_API_KEY") - if not self.api_key: - raise ValueError("FLASHPOINT_API_KEY is required") - self.headers.update({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": f"Bearer {self.api_key}", - }) - - @log_method_call - async def search_communities(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint communities data. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return await self.post("/sources/v2/communities", json={"query": query, **kwargs}) - - @log_method_call - async def search_fraud(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint fraud datasets. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return await self.post("/sources/v2/fraud", json={"query": query, **kwargs}) - - @log_method_call - async def search_marketplaces(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint marketplace datasets. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return await self.post("/sources/v2/markets", json={"query": query, **kwargs}) - - @log_method_call - async def search_media(self, query: str, **kwargs) -> httpx.Response: - """ - Search OCR-processed media from Flashpoint. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return await self.post("/sources/v2/media", json={"query": query, **kwargs}) - - @log_method_call - async def get_media_object(self, query: str) -> httpx.Response: - """ - Retrieve metadata for a specific media object. - - Args: - query (str): The media_id of the object to retrieve. - """ - return await self.get(f"/sources/v2/media/{query}") - - @log_method_call - async def get_media_image(self, query: str) -> httpx.Response: - """ - Download image asset by storage_uri. - - Args: - query (str): The storage_uri (asset_id) of the image to download. - """ - safe_headers = {"Authorization": f"Bearer {self.api_key}"} - return await self.get("/sources/v1/media/", headers=safe_headers, params={"asset_id": query}) - - @log_method_call - async def search_checks(self, query: str, **kwargs) -> httpx.Response: - """ - Search Flashpoint fraud check datasets asynchronously. - - Args: - query (str): The search string used in the API query. - **kwargs: Additional query logic per the Flashpoint API documentation. - """ - return await self.post("/sources/v2/fraud/checks", json={"query": query, **kwargs}) \ No newline at end of file diff --git a/ppp_connectors/api_connectors/generic.py b/ppp_connectors/api_connectors/generic.py deleted file mode 100644 index 094fb15..0000000 --- a/ppp_connectors/api_connectors/generic.py +++ /dev/null @@ -1,105 +0,0 @@ -import httpx -from typing import Dict, Any, Optional -from ppp_connectors.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call - -@bubble_broker_init_signature() -class GenericConnector(Broker): - """ - A flexible, minimal connector that allows sending arbitrary HTTP requests - using the Broker infrastructure. - """ - - @log_method_call - def request( - self, - method: str, - url: str, - headers: Optional[Dict[str, str]] = None, - params: Optional[Dict[str, Any]] = None, - data: Optional[Any] = None, - json: Optional[Dict[str, Any]] = None, - auth: Optional[Any] = None, - timeout: Optional[int] = None, - **kwargs - ) -> httpx.Response: - """ - Make an arbitrary HTTP request using Broker's request logic. - - Args: - method (str): HTTP method (e.g., GET, POST). - url (str): Fully qualified URL to send the request to. - headers (Optional[Dict[str, str]]): Request headers. - params (Optional[Dict[str, Any]]): Query string parameters. - data (Optional[Any]): Form-encoded or raw data. - json (Optional[Dict[str, Any]]): JSON payload. - auth (Optional[Any]): Authentication (e.g., tuple for basic auth). - timeout (Optional[int]): Request timeout in seconds. - - Returns: - httpx.Response: The response object. - """ - - # Merge headers with base class - merged_headers = self.headers.copy() - if headers: - merged_headers.update(headers) - - return self._make_request( - method=method, - endpoint=url, - headers=merged_headers, - params=params, - json=json, - auth=auth, - retry_kwargs=kwargs.get("retry_kwargs"), - ) - - -@bubble_broker_init_signature() -class AsyncGenericConnector(AsyncBroker): - """ - Async version of GenericConnector using AsyncBroker infrastructure. - """ - - @log_method_call - async def request( - self, - method: str, - url: str, - headers: Optional[Dict[str, str]] = None, - params: Optional[Dict[str, Any]] = None, - data: Optional[Any] = None, - json: Optional[Dict[str, Any]] = None, - auth: Optional[Any] = None, - timeout: Optional[int] = None, - **kwargs - ) -> httpx.Response: - """ - Make an arbitrary HTTP request using AsyncBroker's request logic. - - Args: - method (str): HTTP method (e.g., GET, POST). - url (str): Fully qualified URL to send the request to. - headers (Optional[Dict[str, str]]): Request headers. - params (Optional[Dict[str, Any]]): Query string parameters. - data (Optional[Any]): Form-encoded or raw data. - json (Optional[Dict[str, Any]]): JSON payload. - auth (Optional[Any]): Authentication (e.g., tuple for basic auth). - timeout (Optional[int]): Request timeout in seconds. - - Returns: - httpx.Response: The response object. - """ - merged_headers = self.headers.copy() - if headers: - merged_headers.update(headers) - - return await self._make_request( - method=method, - endpoint=url, - headers=merged_headers, - params=params, - json=json, - auth=auth, - retry_kwargs=kwargs.get("retry_kwargs"), - ) diff --git a/ppp_connectors/api_connectors/ipqs.py b/ppp_connectors/api_connectors/ipqs.py deleted file mode 100644 index e627732..0000000 --- a/ppp_connectors/api_connectors/ipqs.py +++ /dev/null @@ -1,68 +0,0 @@ -import httpx -from typing import Optional -from urllib.parse import quote -from ppp_connectors.api_connectors.broker import Broker, AsyncBroker, bubble_broker_init_signature, log_method_call - -@bubble_broker_init_signature() -class IPQSConnector(Broker): - """ - A connector for the IPQualityScore Malicious URL Scanner API. - - This class provides a typed interface to interact with IPQS's malicious URL - scan endpoint. It handles API key management, header setup, and request routing - through the shared Broker infrastructure. - - Attributes: - api_key (str): The API key used to authenticate with IPQS. - """ - def __init__(self, api_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://ipqualityscore.com/api/json", **kwargs) - - self.api_key = api_key or self.env_config.get("IPQS_API_KEY") - if not self.api_key: - raise ValueError("API key is required for IPQSConnector") - self.headers.update({"Content-Type": "application/json"}) - - @log_method_call - def malicious_url(self, query: str, **kwargs) -> httpx.Response: - """ - Scan a URL using IPQualityScore's Malicious URL Scanner API. - - Args: - query (str): The URL to scan (will be URL-encoded). - **kwargs: Optional parameters like 'strictness' or 'fast' to influence scan behavior. - - Returns: - httpx.Response: the httpx.Response object - """ - encoded_query: str = quote(query, safe="") - return self.post(f"/url/", json={"url": query, "key": self.api_key, **kwargs}) - - -@bubble_broker_init_signature() -class AsyncIPQSConnector(AsyncBroker): - """ - Async version of IPQSConnector using AsyncBroker infrastructure. - """ - def __init__(self, api_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://ipqualityscore.com/api/json", **kwargs) - - self.api_key = api_key or self.env_config.get("IPQS_API_KEY") - if not self.api_key: - raise ValueError("API key is required for AsyncIPQSConnector") - self.headers.update({"Content-Type": "application/json"}) - - @log_method_call - async def malicious_url(self, query: str, **kwargs) -> httpx.Response: - """ - Asynchronously scan a URL using IPQualityScore's Malicious URL Scanner API. - - Args: - query (str): The URL to scan (will be URL-encoded). - **kwargs: Optional parameters like 'strictness' or 'fast' to influence scan behavior. - - Returns: - httpx.Response: the httpx.Response object - """ - encoded_query: str = quote(query, safe="") - return await self.post(f"/url/", json={"url": query, "key": self.api_key, **kwargs}) diff --git a/ppp_connectors/api_connectors/spycloud.py b/ppp_connectors/api_connectors/spycloud.py deleted file mode 100644 index cfd40a7..0000000 --- a/ppp_connectors/api_connectors/spycloud.py +++ /dev/null @@ -1,207 +0,0 @@ -import httpx -from typing import Dict, Any, Optional -from ppp_connectors.api_connectors.broker import AsyncBroker, Broker, bubble_broker_init_signature, log_method_call - - -@bubble_broker_init_signature() -class SpycloudConnector(Broker): - """ - SpyCloudConnector provides typed methods to interact with various SpyCloud APIs, including: - - SIP Cookie Domains - - ATO Breach Catalog - - ATO Search - - Investigations Search - """ - - def __init__(self, sip_key: Optional[str] = None, ato_key: Optional[str] = None, - inv_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://api.spycloud.io", **kwargs) - self.sip_key = sip_key or self.env_config.get("SPYCLOUD_API_SIP_KEY") - self.ato_key = ato_key or self.env_config.get("SPYCLOUD_API_ATO_KEY") - self.inv_key = inv_key or self.env_config.get("SPYCLOUD_API_INV_KEY") - - @log_method_call - def sip_cookie_domains(self, cookie_domains: str, **kwargs) -> httpx.Response: - """Query SIP cookie domain data.""" - if not self.sip_key: - raise ValueError("SPYCLOUD_API_SIP_KEY is required for this request.") - endpoint = f"/sip-v1/breach/data/cookie-domains/{cookie_domains}" - headers = { - "accept": "application/json", - "x-api-key": self.sip_key - } - return self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) - - @log_method_call - def ato_breach_catalog(self, query: str, **kwargs) -> httpx.Response: - """Query ATO breach catalog.""" - if not self.ato_key: - raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") - endpoint = "/sp-v2/breach/catalog" - headers = { - "accept": "application/json", - "x-api-key": self.ato_key - } - params = {"query": query, **kwargs} - return self._make_request("get", endpoint=endpoint, headers=headers, params=params) - - @log_method_call - def ato_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: - """Search against SpyCloud's ATO breach dataset.""" - if not self.ato_key: - raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") - - base_url = "/sp-v2/breach/data" - endpoints = { - 'domain': 'domains', - 'email': 'emails', - 'ip': 'ips', - 'username': 'usernames', - 'phone-number': 'phone-numbers', - } - - if search_type not in endpoints: - raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') - - endpoint = f"{base_url}/{endpoints[search_type]}/{query}" - headers = { - "accept": "application/json", - "x-api-key": self.ato_key - } - return self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) - - @log_method_call - def investigations_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: - """Search SpyCloud Investigations API by type and query.""" - if not self.inv_key: - raise ValueError("SPYCLOUD_API_INV_KEY is required for this request.") - - base_url = "/investigations-v2/breach/data" - endpoints = { - 'domain': 'domains', - 'email': 'emails', - 'ip': 'ips', - 'infected-machine-id': 'infected-machine-ids', - 'log-id': 'log-ids', - 'password': 'passwords', - 'username': 'usernames', - 'email-username': 'email-usernames', - 'phone-number': 'phone-numbers', - 'social-handle': 'social-handles', - 'bank-number': 'bank-numbers', - 'cc-number': 'cc-numbers', - 'drivers-license': 'drivers-licenses', - 'national-id': 'national-ids', - 'passport-number': 'passport-numbers', - 'ssn': 'social-security-numbers', - } - - if search_type not in endpoints: - raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') - - endpoint = f"{base_url}/{endpoints[search_type]}/{query}" - headers = { - "accept": "application/json", - "x-api-key": self.inv_key - } - return self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) - - -@bubble_broker_init_signature() -class AsyncSpycloudConnector(AsyncBroker): - """ - Async version of SpycloudConnector. - """ - - def __init__(self, sip_key: Optional[str] = None, ato_key: Optional[str] = None, - inv_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://api.spycloud.io", **kwargs) - self.sip_key = sip_key or self.env_config.get("SPYCLOUD_API_SIP_KEY") - self.ato_key = ato_key or self.env_config.get("SPYCLOUD_API_ATO_KEY") - self.inv_key = inv_key or self.env_config.get("SPYCLOUD_API_INV_KEY") - - @log_method_call - async def sip_cookie_domains(self, cookie_domains: str, **kwargs) -> httpx.Response: - """Query SIP cookie domain data (async).""" - if not self.sip_key: - raise ValueError("SPYCLOUD_API_SIP_KEY is required for this request.") - endpoint = f"/sip-v1/breach/data/cookie-domains/{cookie_domains}" - headers = { - "accept": "application/json", - "x-api-key": self.sip_key - } - return await self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) - - @log_method_call - async def ato_breach_catalog(self, query: str, **kwargs) -> httpx.Response: - """Query ATO breach catalog (async).""" - if not self.ato_key: - raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") - endpoint = "/sp-v2/breach/catalog" - headers = { - "accept": "application/json", - "x-api-key": self.ato_key - } - params = {"query": query, **kwargs} - return await self._make_request("get", endpoint=endpoint, headers=headers, params=params) - - @log_method_call - async def ato_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: - """Search against SpyCloud's ATO breach dataset (async).""" - if not self.ato_key: - raise ValueError("SPYCLOUD_API_ATO_KEY is required for this request.") - - base_url = "/sp-v2/breach/data" - endpoints = { - 'domain': 'domains', - 'email': 'emails', - 'ip': 'ips', - 'username': 'usernames', - 'phone-number': 'phone-numbers', - } - - if search_type not in endpoints: - raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') - - endpoint = f"{base_url}/{endpoints[search_type]}/{query}" - headers = { - "accept": "application/json", - "x-api-key": self.ato_key - } - return await self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) - - @log_method_call - async def investigations_search(self, search_type: str, query: str, **kwargs) -> httpx.Response: - """Search SpyCloud Investigations API by type and query (async).""" - if not self.inv_key: - raise ValueError("SPYCLOUD_API_INV_KEY is required for this request.") - - base_url = "/investigations-v2/breach/data" - endpoints = { - 'domain': 'domains', - 'email': 'emails', - 'ip': 'ips', - 'infected-machine-id': 'infected-machine-ids', - 'log-id': 'log-ids', - 'password': 'passwords', - 'username': 'usernames', - 'email-username': 'email-usernames', - 'phone-number': 'phone-numbers', - 'social-handle': 'social-handles', - 'bank-number': 'bank-numbers', - 'cc-number': 'cc-numbers', - 'drivers-license': 'drivers-licenses', - 'national-id': 'national-ids', - 'passport-number': 'passport-numbers', - 'ssn': 'social-security-numbers', - } - - if search_type not in endpoints: - raise ValueError(f'Invalid search_type: {search_type}. Must be one of: {", ".join(endpoints.keys())}') - - endpoint = f"{base_url}/{endpoints[search_type]}/{query}" - headers = { - "accept": "application/json", - "x-api-key": self.inv_key - } - return await self._make_request("get", endpoint=endpoint, headers=headers, params=kwargs) \ No newline at end of file diff --git a/ppp_connectors/api_connectors/twilio.py b/ppp_connectors/api_connectors/twilio.py deleted file mode 100644 index 9e03160..0000000 --- a/ppp_connectors/api_connectors/twilio.py +++ /dev/null @@ -1,114 +0,0 @@ -from datetime import date, datetime -from typing import Dict, Any, List, Set, Union, Optional -from httpx import BasicAuth, Response -from ppp_connectors.api_connectors.broker import AsyncBroker, Broker, bubble_broker_init_signature, log_method_call -from ppp_connectors.helpers import validate_date_string - -@bubble_broker_init_signature() -class TwilioConnector(Broker): - """ - Connector for interacting with Twilio Lookup and Usage APIs. - - Supports phone number lookups with data packages and generating account usage reports. - """ - - def __init__( - self, - api_sid: Optional[str] = None, - api_secret: Optional[str] = None, - **kwargs - ): - super().__init__(base_url="https://lookups.twilio.com/v2", **kwargs) - - self.api_sid = api_sid or self.env_config.get("TWILIO_API_SID") - self.api_secret = api_secret or self.env_config.get("TWILIO_API_SECRET") - - if not self.api_sid or not self.api_secret: - raise ValueError("TWILIO_API_SID and TWILIO_API_SECRET are required.") - - self.auth = BasicAuth(self.api_sid, self.api_secret) - - @log_method_call - def lookup_phone(self, phone_number: str, data_packages: Optional[List[str]] = None, **kwargs) -> Response: - """ - Query information about a phone number using Twilio's Lookup API. - - Args: - phone_number (str): The phone number to query. - data_packages (list): Optional data packages to include (e.g. 'caller_name', 'sim_swap'). - - Returns: - httpx.Response: the httpx.Response object - """ - valid_data_packages: Set[str] = { - 'caller_name', 'sim_swap', 'call_forwarding', 'line_status', - 'line_type_intelligence', 'identity_match', 'reassigned_number', - 'sms_pumping_risk', 'phone_number_quality_score', 'pre_fill' - } - - if data_packages: - invalid = set(data_packages) - valid_data_packages - if invalid: - raise ValueError(f"Invalid data packages: {', '.join(invalid)}") - - params: Dict[str, Any] = { - 'Fields': ','.join(data_packages) if data_packages else "", - **kwargs - } - - endpoint = f"/PhoneNumbers/{phone_number}" - return self._make_request("get", endpoint=endpoint, auth=self.auth, params=params) - - -@bubble_broker_init_signature() -class AsyncTwilioConnector(AsyncBroker): - """ - Async connector for interacting with Twilio Lookup API. - """ - - def __init__( - self, - api_sid: Optional[str] = None, - api_secret: Optional[str] = None, - **kwargs - ): - super().__init__(base_url="https://lookups.twilio.com/v2", **kwargs) - - self.api_sid = api_sid or self.env_config.get("TWILIO_API_SID") - self.api_secret = api_secret or self.env_config.get("TWILIO_API_SECRET") - - if not self.api_sid or not self.api_secret: - raise ValueError("TWILIO_API_SID and TWILIO_API_SECRET are required.") - - self.auth = BasicAuth(self.api_sid, self.api_secret) - - @log_method_call - async def lookup_phone(self, phone_number: str, data_packages: Optional[List[str]] = None, **kwargs) -> Response: - """ - Async version of phone number lookup using Twilio Lookup API. - - Args: - phone_number (str): The phone number to query. - data_packages (list): Optional data packages to include (e.g. 'caller_name', 'sim_swap'). - - Returns: - httpx.Response: the httpx.Response object - """ - valid_data_packages: Set[str] = { - 'caller_name', 'sim_swap', 'call_forwarding', 'line_status', - 'line_type_intelligence', 'identity_match', 'reassigned_number', - 'sms_pumping_risk', 'phone_number_quality_score', 'pre_fill' - } - - if data_packages: - invalid = set(data_packages) - valid_data_packages - if invalid: - raise ValueError(f"Invalid data packages: {', '.join(invalid)}") - - params: Dict[str, Any] = { - 'Fields': ','.join(data_packages) if data_packages else "", - **kwargs - } - - endpoint = f"/PhoneNumbers/{phone_number}" - return await self._make_request("get", endpoint=endpoint, auth=self.auth, params=params) \ No newline at end of file diff --git a/ppp_connectors/api_connectors/urlscan.py b/ppp_connectors/api_connectors/urlscan.py deleted file mode 100644 index f68e181..0000000 --- a/ppp_connectors/api_connectors/urlscan.py +++ /dev/null @@ -1,148 +0,0 @@ -import httpx -from typing import Dict, Any, Optional -from ppp_connectors.api_connectors.broker import AsyncBroker, Broker, bubble_broker_init_signature, log_method_call - -@bubble_broker_init_signature() -class URLScanConnector(Broker): - """ - A connector for interacting with the urlscan.io API. - - Provides structured methods for submitting scans, querying historical data, - and retrieving detailed scan results and metadata. - - Attributes: - api_key (str): The API key used to authenticate with urlscan.io. - """ - - def __init__(self, api_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://urlscan.io", **kwargs) - - self.api_key = api_key or self.env_config.get("URLSCAN_API_KEY") - if not self.api_key: - raise ValueError("API key is required for URLScanConnector") - self.headers.update({ - "accept": "application/json", - "API-Key": self.api_key - }) - - @log_method_call - def search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Search for archived scans matching a given query. - - Args: - query (str): The search term or filter string. - **kwargs: Additional query parameters for filtering results. - - Returns: - httpx.Response: the httpx.Response object - """ - params = {"q": query, **kwargs} - return self.get("/api/v1/search/", params=params) - - @log_method_call - def scan(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Submit a URL to be scanned by urlscan.io. - - Args: - query (str): The URL to scan. - **kwargs: Additional scan options like tags, visibility, or referer. - - Returns: - httpx.Response: the httpx.Response object - """ - payload = {"url": query, **kwargs} - return self.post("/api/v1/scan", json=payload) - - @log_method_call - def results(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Retrieve detailed scan results by UUID. - - Args: - query (str): The UUID of the scan. - - Returns: - httpx.Response: the httpx.Response object - """ - return self.get(f"/api/v1/result/{query}", params=kwargs) - - @log_method_call - def get_dom(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Retrieve the DOM snapshot for a given scan UUID. - - Args: - query (str): The UUID of the scan. - - Returns: - httpx.Response: the httpx.Response object - """ - return self.get(f"/dom/{query}", params=kwargs) - - @log_method_call - def structure_search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Search for scans structurally similar to a given UUID. - - Args: - query (str): The UUID of the original scan. - - Returns: - httpx.Response: the httpx.Response object - """ - return self.get(f"/api/v1/pro/result/{query}/similar", params=kwargs) - -class AsyncURLScanConnector(AsyncBroker): - """ - An async connector for interacting with the urlscan.io API. - """ - - def __init__(self, api_key: Optional[str] = None, **kwargs): - super().__init__(base_url="https://urlscan.io", **kwargs) - - self.api_key = api_key or self.env_config.get("URLSCAN_API_KEY") - if not self.api_key: - raise ValueError("API key is required for AsyncURLScanConnector") - self.headers.update({ - "accept": "application/json", - "API-Key": self.api_key - }) - - @log_method_call - async def search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Async search for archived scans matching a given query. - """ - params = {"q": query, **kwargs} - return await self.get("/api/v1/search/", params=params) - - @log_method_call - async def scan(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Async submit a URL to be scanned by urlscan.io. - """ - payload = {"url": query, **kwargs} - return await self.post("/api/v1/scan", json=payload) - - @log_method_call - async def results(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Async retrieve detailed scan results by UUID. - """ - return await self.get(f"/api/v1/result/{query}", params=kwargs) - - @log_method_call - async def get_dom(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Async retrieve the DOM snapshot for a given scan UUID. - """ - return await self.get(f"/dom/{query}", params=kwargs) - - @log_method_call - async def structure_search(self, query: str, **kwargs: Dict[str, Any]) -> httpx.Response: - """ - Async search for scans structurally similar to a given UUID. - """ - return await self.get(f"/api/v1/pro/result/{query}/similar", params=kwargs) diff --git a/ppp_connectors/broker.py b/ppp_connectors/broker.py deleted file mode 100644 index 896dad4..0000000 --- a/ppp_connectors/broker.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Shim for backward compatibility. -Forwards to ppp_connectors.connectors.broker. -TODO: Deprecate this file in a future major release. -""" - -import warnings -from ppp_connectors.connectors.broker import * - -warnings.warn( - "ppp_connectors.broker is deprecated and will be removed in a future release; use ppp_connectors.connectors.broker instead.", - DeprecationWarning, - stacklevel=2 -) \ No newline at end of file diff --git a/ppp_connectors/dbms_connectors/__init__.py b/ppp_connectors/dbms_connectors/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ppp_connectors/dbms_connectors/elasticsearch.py b/ppp_connectors/dbms_connectors/elasticsearch.py deleted file mode 100644 index 67e2049..0000000 --- a/ppp_connectors/dbms_connectors/elasticsearch.py +++ /dev/null @@ -1,143 +0,0 @@ -from elasticsearch import Elasticsearch, helpers -from typing import List, Dict, Generator, Any, Optional, Union - - -try: - from ppp_connectors.helpers import setup_logger - _default_logger = setup_logger(name="elasticsearch") -except ImportError: - import logging - _default_logger = logging.getLogger("elasticsearch") - if not _default_logger.handlers: - handler = logging.StreamHandler() - formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s') - handler.setFormatter(formatter) - _default_logger.addHandler(handler) - _default_logger.setLevel(logging.INFO) - - -class ElasticsearchConnector: - """ - A connector class for interacting with Elasticsearch. - - This class provides methods to perform paginated search queries using the scroll API - and to execute bulk insert operations. It includes integrated logging support for observability. - """ - def __init__( - self, - hosts: List[str], - username: Optional[str] = None, - password: Optional[str] = None, - logger: Optional[Any] = None - ): - """ - Initialize the Elasticsearch client. - - Args: - hosts (List[str]): List of Elasticsearch host URLs. - username (Optional[str]): Username for basic authentication. Defaults to None. - password (Optional[str]): Password for basic authentication. Defaults to None. - logger (Optional[Any]): Optional logger instance. If not provided, a default logger is used. - """ - self.client = Elasticsearch(hosts, basic_auth=(username, password)) - self.logger = logger if logger is not None else _default_logger - - def _log(self, msg: str, level: str = "info"): - """ - Internal helper to log messages using the provided or default logger. - - Args: - msg (str): The message to log. - level (str): The logging level as a string (e.g., 'info', 'error'). Defaults to 'info'. - """ - if self.logger: - log_method = getattr(self.logger, level, self.logger.info) - log_method(msg) - - def query( - self, - index: str, - query: Union[str, Dict], - size: int = 1000 - ) -> Generator[Dict[str, Any], None, None]: - """ - Execute a paginated search query using the Elasticsearch scroll API. - - This method handles retrieval of large result sets by paging through results - using a scroll context. - - Args: - index (str): The name of the index to search. - query (Union[str, Dict]): A Lucene query string or Elasticsearch DSL query body. - size (int): Number of results to retrieve per batch. Defaults to 1000. - - Yields: - Generator[Dict[str, Any], None, None]: A generator that yields each search hit as a dictionary. - - Note: - This method returns a generator. If you want to collect all results, - you can wrap the result in `list()`, but beware of memory usage if the - result set is large. Prefer streaming and processing results incrementally. - """ - - self._log(f"Executing query on index '{index}' with batch size {size}", "info") - - if isinstance(query, str): - query = { - "query": { - "query_string": { - "query": query - } - } - } - - page = self.client.search(index=index, body=query, scroll="2m", size=size) - sid = page["_scroll_id"] - hits = page["hits"]["hits"] - yield from hits - - while hits: - page = self.client.scroll(scroll_id=sid, scroll="2m") - sid = page["_scroll_id"] - hits = page["hits"]["hits"] - if not hits: - break - yield from hits - self.client.clear_scroll(scroll_id=sid) - self._log(f"Completed scrolling query on index '{index}'", "info") - - def bulk_insert( - self, - index: str, - data: List[Dict], - id_key: str = "_id" - ): - """ - Perform a bulk insert operation into the specified Elasticsearch index. - - This method sends batches of documents for indexing in a single API call. - Each document can optionally specify an ID via the `id_key`. - - Args: - index (str): The name of the index to insert documents into. - data (List[Dict]): A list of documents to insert. - id_key (str): The key in each document to use as the document ID. Defaults to "_id". - - Returns: - Tuple[int, List[Dict]]: A tuple containing the number of successfully processed actions - and a list of any errors encountered during insertion. - """ - self._log(f"Inserting {len(data)} documents into index '{index}'", "info") - actions = [ - { - "_index": index, - "_id": doc.get(id_key), - "_source": doc - } for doc in data - ] - success, errors = helpers.bulk(self.client, actions) - if errors: - self._log(f"Bulk insert encountered errors: {errors}", "error") - else: - self._log("Bulk insert completed successfully", "info") - return success, errors diff --git a/ppp_connectors/dbms_connectors/mongo.py b/ppp_connectors/dbms_connectors/mongo.py deleted file mode 100644 index 83f6e8a..0000000 --- a/ppp_connectors/dbms_connectors/mongo.py +++ /dev/null @@ -1,390 +0,0 @@ -from pymongo import MongoClient, UpdateOne -from pymongo.errors import ( - OperationFailure, - ServerSelectionTimeoutError, - AutoReconnect, - ConnectionFailure, -) -from tenacity import Retrying, stop_after_attempt, wait_fixed, retry_if_exception_type -from typing import List, Dict, Any, Optional, Generator, Type, Union -from types import TracebackType -from ppp_connectors.helpers import setup_logger - - -_DEFAULT_LOGGER = object() - - -class MongoConnector: - """ - A connector class for interacting with MongoDB. - - Provides methods for finding documents with paging and performing batched - insert and upsert operations, as well as convenience helpers for - `distinct`, `delete`, and `delete_many`. - - Supports explicit lifecycle management via `close()` and can be used as a - context manager (`with MongoConnector(...) as conn:`). On initialization, - the connector pings the server to validate connectivity/authentication with - a simple retry policy. - Logs actions if a logger is provided. - """ - def __init__( - self, - uri: str, - username: Optional[str] = None, - password: Optional[str] = None, - auth_source: str = "admin", - timeout: int = 10, - auth_mechanism: Optional[str] = "DEFAULT", - ssl: Optional[bool] = True, - logger: Optional[Any] = _DEFAULT_LOGGER, - auth_retry_attempts: int = 3, - auth_retry_wait: float = 1.0, - ): - """ - Initialize the MongoDB client. - - Args: - uri (str): The MongoDB connection URI. - username (Optional[str]): Username for authentication. Defaults to None. - password (Optional[str]): Password for authentication. Defaults to None. - auth_source (str): The authentication database. Defaults to "admin". - timeout (int): Server selection timeout in seconds. Defaults to 10. - auth_mechanism (Optional[str]): Authentication mechanism for MongoDB (e.g., "SCRAM-SHA-1"). - ssl (Optional[bool]): Whether to use SSL for the connection. - logger (Optional[Any]): Logger instance for logging actions. Defaults to a module logger when omitted; pass None to disable logging. - auth_retry_attempts (int): Number of attempts for initial auth ping. Defaults to 3. - auth_retry_wait (float): Seconds to wait between auth attempts. Defaults to 1.0. - """ - # Initialize MongoClient with authSource, authMechanism, and ssl options - self.client = MongoClient( - uri, - username=username, - password=password, - authSource=auth_source, - authMechanism=auth_mechanism, - ssl=ssl, - serverSelectionTimeoutMS=timeout * 1000 - ) - self.logger = setup_logger(__name__) if logger is _DEFAULT_LOGGER else logger - self.auth_retry_attempts = auth_retry_attempts - self.auth_retry_wait = auth_retry_wait - self._log( - f"Initialized MongoClient with authSource={auth_source}, " - f"authMechanism={auth_mechanism}, ssl={ssl}" - ) - # Force an initial ping to trigger auth/handshake; retry to handle - # clusters that intermittently fail the first attempt. - self._ping_with_retry() - - def close(self) -> None: - """Close the underlying MongoClient connection.""" - self._log("Closing MongoClient connection", level="debug") - try: - self.client.close() - except Exception as exc: - # Closing should be best-effort; log and continue - self._log(f"Error during MongoClient.close(): {exc}", level="warning") - - # Context manager support - def __enter__(self) -> "MongoConnector": - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc: Optional[BaseException], - tb: Optional[TracebackType], - ) -> None: - self.close() - - def _log(self, msg: str, level: str = "info"): - """ - Internal helper method for logging. - - Args: - msg (str): The message to log. - level (str): Logging level as string (e.g., "info", "debug"). Defaults to "info". - """ - if self.logger: - log_method = getattr(self.logger, level, self.logger.info) - log_method(msg) - - def _ping_with_retry(self) -> None: - """Ping the server to validate connection/auth, with retry.""" - for attempt in Retrying( - stop=stop_after_attempt(self.auth_retry_attempts), - wait=wait_fixed(self.auth_retry_wait), - reraise=True, - retry=retry_if_exception_type( - (OperationFailure, ServerSelectionTimeoutError, AutoReconnect, ConnectionFailure) - ), - ): - with attempt: - self._log("Pinging MongoDB to verify connection/auth...", level="debug") - # 'ping' triggers handshake and, when needed, authentication - self.client.admin.command("ping") - - def find( - self, - db_name: str, - collection: str, - filter: Dict, - projection: Optional[Dict] = None, - batch_size: int = 1000 - ) -> Generator[Dict[str, Any], None, None]: - """ - Find documents in a MongoDB collection with optional projection and paging. - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - filter (Dict): MongoDB filter document. - projection (Optional[Dict]): Fields to include or exclude. Defaults to None. - batch_size (int): Number of documents per batch. Defaults to 1000. - - Yields: - Dict[str, Any]: Each document as a dictionary. - - Logs: - Logs the find operation with filter details. - """ - self._log(f"Executing Mongo find on {db_name}.{collection}") - col = self.client[db_name][collection] - cursor = col.find(filter, projection).batch_size(batch_size) - for doc in cursor: - yield doc - - def aggregate( - self, - db_name: str, - collection: str, - pipeline: List[Dict[str, Any]], - batch_size: Optional[int] = None, - **kwargs: Any, - ) -> Generator[Dict[str, Any], None, None]: - """ - Run an aggregation pipeline on a collection and stream results. - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - pipeline (List[Dict[str, Any]]): Aggregation pipeline stages. - batch_size (Optional[int]): If provided, set cursor batch size. - **kwargs: Additional options forwarded to `Collection.aggregate` (e.g., - allowDiskUse, collation, maxTimeMS, comment). - - Yields: - Dict[str, Any]: Each document from the aggregation result. - """ - self._log( - f"Executing Mongo aggregate on {db_name}.{collection}" - ) - col = self.client[db_name][collection] - cursor = col.aggregate(pipeline, **kwargs) - if batch_size is not None: - cursor = cursor.batch_size(batch_size) - for doc in cursor: - yield doc - - def query( - self, - db_name: str, - collection: str, - query: Dict, - projection: Optional[Dict] = None, - batch_size: int = 1000 - ) -> Generator[Dict[str, Any], None, None]: - """ - Deprecated: use `find` instead. - - Backwards-compatible wrapper that forwards to `find`. - """ - self._log( - "MongoConnector.query is deprecated; use MongoConnector.find instead", - level="warning", - ) - return self.find( - db_name=db_name, - collection=collection, - filter=query, - projection=projection, - batch_size=batch_size, - ) - - def insert_many( - self, - db_name: str, - collection: str, - data: List[Dict], - ordered: bool = False, - batch_size: int = 1000, - ): - """ - Insert multiple documents into a collection (batched). - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - data (List[Dict]): Documents to insert. - ordered (bool): Whether operations should be ordered. Defaults to False. - batch_size (int): Number of documents per batch. Defaults to 1000. - - Returns: - List: List of InsertManyResult objects for each batch. - - Note: - PyMongo batches writes internally; manual batching here is useful - for memory control, error isolation, and progress logging. - """ - self._log( - f"insert_many: inserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}" - ) - col = self.client[db_name][collection] - results = [] - for i in range(0, len(data), batch_size): - batch = data[i:i + batch_size] - result = col.insert_many(batch, ordered=ordered) - results.append(result) - return results - - def upsert_many( - self, - db_name: str, - collection: str, - data: List[Dict], - unique_key: Optional[Union[str, List[str]]], - ordered: bool = False, - batch_size: int = 1000, - ): - """ - Upsert multiple documents into a collection using a unique key or keys (batched). - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - data (List[Dict]): Documents to upsert. - unique_key (Optional[Union[str, List[str]]]): Field name or list of field names to use for upsert filtering. - If a list, the filter is built as a compound key using all specified fields. - ordered (bool): Whether operations should be ordered. Defaults to False. - batch_size (int): Number of documents per batch. Defaults to 1000. - - Returns: - List: List of BulkWriteResult objects for each batch. - - Details: - Uses `bulk_write` with `UpdateOne(filter, {"$set": doc}, upsert=True)` - to merge fields from each document. The `unique_key` or all keys in the list - must exist in each document. - """ - if not unique_key: - raise ValueError("unique_key must be provided for upsert_many") - self._log( - f"upsert_many: upserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}, unique_key={unique_key}" - ) - col = self.client[db_name][collection] - results = [] - for i in range(0, len(data), batch_size): - batch = data[i:i + batch_size] - operations = [] - for doc in batch: - if isinstance(unique_key, str): - if unique_key in doc: - filter_doc = {unique_key: doc[unique_key]} - else: - continue - elif isinstance(unique_key, list): - if all(k in doc for k in unique_key): - filter_doc = {k: doc[k] for k in unique_key} - else: - continue - else: - raise ValueError("unique_key must be either a string or a list of strings") - operations.append(UpdateOne(filter_doc, {"$set": doc}, upsert=True)) - if operations: - result = col.bulk_write(operations, ordered=ordered) - results.append(result) - return results - - def distinct( - self, - db_name: str, - collection: str, - key: str, - filter: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> List[Any]: - """ - Return a list of distinct values for `key` across documents. - - This is a thin wrapper around PyMongo's `Collection.distinct` and accepts - any additional keyword arguments supported by PyMongo (e.g., collation, - maxTimeMS, comment). - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - key (str): Field name for which to return distinct values. - filter (Optional[Dict[str, Any]]): Optional query filter to limit the scope. - **kwargs: Additional options forwarded to `Collection.distinct`. - - Returns: - List[Any]: Distinct values for the specified key. - """ - self._log( - f"Executing Mongo distinct on {db_name}.{collection} for key='{key}'" - ) - col = self.client[db_name][collection] - return col.distinct(key, filter, **kwargs) - - def delete( - self, - db_name: str, - collection: str, - filter: Dict[str, Any], - **kwargs: Any, - ): - """ - Delete a single document matching the filter (wrapper over delete_one). - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - filter (Dict[str, Any]): Query filter selecting the document to delete. - **kwargs: Additional options forwarded to `Collection.delete_one` (e.g., collation, comment). - - Returns: - DeleteResult: The result of the delete operation. - """ - self._log( - f"Deleting one from {db_name}.{collection}", - level="info", - ) - col = self.client[db_name][collection] - return col.delete_one(filter, **kwargs) - - def delete_many( - self, - db_name: str, - collection: str, - filter: Dict[str, Any], - **kwargs: Any, - ): - """ - Delete all documents matching the filter (wrapper over delete_many). - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - filter (Dict[str, Any]): Query filter selecting documents to delete. - **kwargs: Additional options forwarded to `Collection.delete_many` (e.g., collation, comment). - - Returns: - DeleteResult: The result of the delete operation. - """ - self._log( - f"Deleting many from {db_name}.{collection}", - level="info", - ) - col = self.client[db_name][collection] - return col.delete_many(filter, **kwargs) diff --git a/ppp_connectors/dbms_connectors/mongo_async.py b/ppp_connectors/dbms_connectors/mongo_async.py deleted file mode 100644 index 5e7550a..0000000 --- a/ppp_connectors/dbms_connectors/mongo_async.py +++ /dev/null @@ -1,323 +0,0 @@ -from __future__ import annotations -from typing import Any, Dict, List, Optional, AsyncIterator, Type, Union -from types import TracebackType -import inspect -from pymongo import UpdateOne -from pymongo.errors import ( - OperationFailure, - ServerSelectionTimeoutError, - AutoReconnect, - ConnectionFailure, -) -from tenacity import AsyncRetrying, stop_after_attempt, wait_fixed, retry_if_exception_type -from ppp_connectors.helpers import setup_logger - - -_DEFAULT_LOGGER = object() - - -class AsyncMongoConnector: - """ - An asyncio connector for interacting with MongoDB using PyMongo's async client. - - Mirrors the synchronous MongoConnector API with async methods and context - management (`async with AsyncMongoConnector(...) as conn:`). On entry, the - connector pings the server with a simple retry policy to validate the - connection and trigger authentication. - - Args: - uri (str): The MongoDB connection URI. - username (Optional[str]): Username for authentication. Defaults to None. - password (Optional[str]): Password for authentication. Defaults to None. - auth_source (str): The authentication database. Defaults to "admin". - timeout (int): Server selection timeout in seconds. Defaults to 10. - auth_mechanism (Optional[str]): Authentication mechanism for MongoDB (e.g., "SCRAM-SHA-1"). - ssl (Optional[bool]): Whether to use SSL for the connection. - logger (Optional[Any]): Logger instance for logging actions. Defaults to a module logger when omitted; pass None to disable logging. - auth_retry_attempts (int): Number of attempts for initial auth ping. Defaults to 3. - auth_retry_wait (float): Seconds to wait between auth attempts. Defaults to 1.0. - """ - - def __init__( - self, - uri: str, - username: Optional[str] = None, - password: Optional[str] = None, - auth_source: str = "admin", - timeout: int = 10, - auth_mechanism: Optional[str] = "DEFAULT", - ssl: Optional[bool] = True, - logger: Optional[Any] = _DEFAULT_LOGGER, - auth_retry_attempts: int = 3, - auth_retry_wait: float = 1.0, - ) -> None: - # Import the asyncio client lazily to provide a clear error if unavailable - AsyncMongoClient = None # type: ignore - import_error: Optional[Exception] = None - try: - from pymongo.asynchronous.mongo_client import AsyncMongoClient # type: ignore - except Exception as e1: # pragma: no cover - fallback path - import_error = e1 - try: - # Some versions may expose it at package level - from pymongo.asynchronous import AsyncMongoClient as _AltAsyncClient # type: ignore - AsyncMongoClient = _AltAsyncClient # type: ignore - except Exception as e2: # pragma: no cover - fallback path - import_error = e2 - try: - # Older preview namespace - from pymongo.asyncio import MongoClient as _AsyncioClient # type: ignore - AsyncMongoClient = _AsyncioClient # type: ignore - except Exception as e3: # pragma: no cover - final fallback - import_error = e3 - - if AsyncMongoClient is None: # type: ignore - raise ImportError( - "PyMongo async client is not available. Ensure a recent pymongo version is installed." - ) from import_error - - self.client = AsyncMongoClient( # type: ignore[misc] - uri, - username=username, - password=password, - authSource=auth_source, - authMechanism=auth_mechanism, - ssl=ssl, - serverSelectionTimeoutMS=timeout * 1000, - ) - self.logger = setup_logger(__name__) if logger is _DEFAULT_LOGGER else logger - self._log( - f"Initialized AsyncMongoClient with authSource={auth_source}, " - f"authMechanism={auth_mechanism}, ssl={ssl}" - ) - self.auth_retry_attempts = auth_retry_attempts - self.auth_retry_wait = auth_retry_wait - - # Async context manager - async def __aenter__(self) -> "AsyncMongoConnector": - await self._ping_with_retry() - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc: Optional[BaseException], - tb: Optional[TracebackType], - ) -> None: - await self.close() - - def _log(self, msg: str, level: str = "info") -> None: - if self.logger: - log_method = getattr(self.logger, level, self.logger.info) - log_method(msg) - - async def _ping_with_retry(self) -> None: - """Async ping to validate connection/auth, with retry.""" - async for attempt in AsyncRetrying( - stop=stop_after_attempt(self.auth_retry_attempts), - wait=wait_fixed(self.auth_retry_wait), - reraise=True, - retry=retry_if_exception_type( - (OperationFailure, ServerSelectionTimeoutError, AutoReconnect, ConnectionFailure) - ), - ): - with attempt: - self._log("Pinging MongoDB (async) to verify connection/auth...", level="debug") - await self.client.admin.command("ping") - - # Operations - async def find( - self, - db_name: str, - collection: str, - filter: Dict[str, Any], - projection: Optional[Dict[str, Any]] = None, - batch_size: int = 1000, - ) -> AsyncIterator[Dict[str, Any]]: - """ - Async find documents with optional projection and paging. - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - filter (Dict[str, Any]): MongoDB filter document. - projection (Optional[Dict[str, Any]]): Fields to include/exclude. - batch_size (int): Number of documents per batch. - - Yields: - Each document as a dictionary. - """ - self._log( - f"Executing async Mongo find on {db_name}.{collection}" - ) - col = self.client[db_name][collection] - cursor = col.find(filter, projection).batch_size(batch_size) - async for doc in cursor: - yield doc - - async def aggregate( - self, - db_name: str, - collection: str, - pipeline: List[Dict[str, Any]], - batch_size: Optional[int] = None, - **kwargs: Any, - ) -> AsyncIterator[Dict[str, Any]]: - """ - Async aggregation pipeline execution yielding documents. - - Args: - db_name (str): Name of the database. - collection (str): Name of the collection. - pipeline (List[Dict[str, Any]]): Aggregation pipeline stages. - batch_size (Optional[int]): If provided, set cursor batch size. - **kwargs: Additional options for `Collection.aggregate` (e.g., allowDiskUse). - - Yields: - Each document from the aggregation result. - - Note: - This returns an async iterator. Use `async for` to consume it. - Do not `await` the return value directly. - """ - self._log( - f"Executing async Mongo aggregate on {db_name}.{collection}" - ) - col = self.client[db_name][collection] - # Some PyMongo async versions require awaiting aggregate() to get a cursor - result = col.aggregate(pipeline, **kwargs) - cursor = await result if inspect.isawaitable(result) else result - if batch_size is not None: - cursor = cursor.batch_size(batch_size) - async for doc in cursor: - yield doc - - async def insert_many( - self, - db_name: str, - collection: str, - data: List[Dict[str, Any]], - ordered: bool = False, - batch_size: int = 1000, - ) -> List[Any]: - """ - Async insert multiple documents into a collection (batched). - - Note: - PyMongo batches writes internally as well; manual batching is - primarily for progress logging, memory control, and error isolation. - """ - self._log( - f"async insert_many: inserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}" - ) - col = self.client[db_name][collection] - results: List[Any] = [] - for i in range(0, len(data), batch_size): - batch = data[i : i + batch_size] - result = await col.insert_many(batch, ordered=ordered) - results.append(result) - return results - - async def upsert_many( - self, - db_name: str, - collection: str, - data: List[Dict[str, Any]], - unique_key: Optional[Union[str, List[str]]], - ordered: bool = False, - batch_size: int = 1000, - ) -> List[Any]: - """ - Async upsert multiple documents using a unique key (batched). - - Uses `bulk_write` with `UpdateOne(..., upsert=True)` and `$set` to - merge fields from each document. - - Args: - unique_key (Union[str, List[str]]): A string key or list of strings representing - the unique key(s) used to build the filter for upsert operations. - If a list is provided, it is treated as a compound unique key. - """ - if not unique_key: - raise ValueError("unique_key must be provided for upsert_many") - if not (isinstance(unique_key, str) or (isinstance(unique_key, list) and all(isinstance(k, str) for k in unique_key))): - raise ValueError("unique_key must be a string or a list of strings") - self._log( - f"async upsert_many: upserting {len(data)} docs into {db_name}.{collection} with batch_size={batch_size}, unique_key={unique_key}" - ) - col = self.client[db_name][collection] - results: List[Any] = [] - for i in range(0, len(data), batch_size): - batch = data[i : i + batch_size] - operations = [] - for doc in batch: - if isinstance(unique_key, str): - if unique_key in doc: - filter_doc = {unique_key: doc[unique_key]} - else: - continue - else: - # unique_key is a list of strings - filter_doc = {k: doc[k] for k in unique_key if k in doc} - if len(filter_doc) != len(unique_key): - continue - operations.append(UpdateOne(filter_doc, {"$set": doc}, upsert=True)) - if operations: - result = await col.bulk_write(operations, ordered=ordered) - results.append(result) - return results - - async def distinct( - self, - db_name: str, - collection: str, - key: str, - filter: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> List[Any]: - """Async distinct values for a key, with optional filter.""" - self._log( - f"Executing async Mongo distinct on {db_name}.{collection} for key='{key}'" - ) - col = self.client[db_name][collection] - return await col.distinct(key, filter, **kwargs) - - async def delete( - self, - db_name: str, - collection: str, - filter: Dict[str, Any], - **kwargs: Any, - ) -> Any: - """Async delete a single document matching the filter.""" - self._log( - f"Async deleting one from {db_name}.{collection}", - level="info", - ) - col = self.client[db_name][collection] - return await col.delete_one(filter, **kwargs) - - async def delete_many( - self, - db_name: str, - collection: str, - filter: Dict[str, Any], - **kwargs: Any, - ) -> Any: - """Async delete all documents matching the filter.""" - self._log( - f"Async deleting many from {db_name}.{collection}", - level="info", - ) - col = self.client[db_name][collection] - return await col.delete_many(filter, **kwargs) - - async def close(self) -> None: - """Close the underlying async client.""" - self._log("Closing AsyncMongoClient connection", level="debug") - try: - result = self.client.close() - if inspect.isawaitable(result): - await result - except Exception as exc: # pragma: no cover - best-effort close - self._log(f"Error during AsyncMongoClient.close(): {exc}", level="warning") diff --git a/ppp_connectors/dbms_connectors/odbc.py b/ppp_connectors/dbms_connectors/odbc.py deleted file mode 100644 index 8b5e157..0000000 --- a/ppp_connectors/dbms_connectors/odbc.py +++ /dev/null @@ -1,110 +0,0 @@ -from importlib import import_module -from typing import Any, Dict, Generator, List -from ppp_connectors.helpers import setup_logger - -_PYODBC_MODULE = None - - -def _get_pyodbc(): - """ - Lazily import pyodbc so the package stays importable without the optional dep. - """ - global _PYODBC_MODULE - if _PYODBC_MODULE is None: - try: - _PYODBC_MODULE = import_module("pyodbc") - except ImportError as exc: - raise ImportError( - "pyodbc is not installed. Install ppp_connectors[odbc] or add pyodbc to your deps." - ) from exc - return _PYODBC_MODULE - - -class ODBCConnector: - """ - A connector class for interacting with ODBC-compatible databases. - - Provides methods for querying and bulk inserts. - Supports use as a context manager for automatic connection cleanup. - """ - - def __init__(self, conn_str: str, logger: Any = None): - """ - Initialize the ODBC connection. - - Args: - conn_str (str): The ODBC connection string. - logger (Any, optional): Logger instance for logging. Defaults to None. - """ - pyodbc = _get_pyodbc() - self.conn = pyodbc.connect(conn_str) - self.logger = logger or setup_logger(__name__) - self._log("ODBC connection established") - - def _log(self, msg: str, level: str = "info"): - if self.logger: - log_method = getattr(self.logger, level, self.logger.info) - log_method(msg) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def close(self): - """Close the ODBC connection.""" - if self.conn: - self.conn.close() - self._log("ODBC connection closed") - - def query(self, base_query: str) -> Generator[Dict[str, Any], None, None]: - """ - Execute a query against an ODBC database and yield each row as a dictionary. - - Args: - base_query (str): The SQL query to execute. - - Yields: - Generator[Dict[str, Any], None, None]: A generator that yields each search hit as a dictionary. - - Logs: - Execution of the query. - - Note: - This method returns a generator. If you want to collect all results, - you can wrap the result in `list()`, but beware of memory usage if the - result set is large. - """ - self._log(f"Executing ODBC query") - cursor = self.conn.cursor() - cursor.execute(base_query) - columns = [col[0] for col in cursor.description] - for row in cursor.fetchall(): - yield dict(zip(columns, row)) - - def bulk_insert(self, table: str, data: List[Dict[str, Any]]): - """ - Perform a bulk insert into an ODBC database table. - - Args: - table (str): Name of the table to insert into. - data (List[Dict[str, Any]]): List of rows to insert. - - Returns: - None - - Logs: - Number of rows inserted and target table. - """ - if not data: - return - self._log(f"Inserting {len(data)} rows into table {table}") - columns = list(data[0].keys()) - placeholders = ", ".join(["?"] * len(columns)) - insert_query = f"INSERT INTO {table} ({', '.join(columns)}) VALUES ({placeholders})" - values = [tuple(row[col] for col in columns) for row in data] - cursor = self.conn.cursor() - cursor.fast_executemany = True - cursor.executemany(insert_query, values) - self.conn.commit() diff --git a/ppp_connectors/dbms_connectors/splunk.py b/ppp_connectors/dbms_connectors/splunk.py deleted file mode 100644 index 78390a7..0000000 --- a/ppp_connectors/dbms_connectors/splunk.py +++ /dev/null @@ -1,131 +0,0 @@ -import httpx -from typing import Generator, Dict, Any, Optional -from ppp_connectors.helpers import setup_logger - - -class SplunkConnector: - """ - A connector class for interacting with Splunk via its REST API. - - Provides methods for submitting search jobs and streaming paginated results. - """ - def __init__( - self, - host: str, - port: int, - username: Optional[str] = None, - password: Optional[str] = None, - scheme: str = "https", - verify: bool = True, - timeout: int = 30, - logger: Optional[Any] = None - ): - """ - Initialize the SplunkConnector with connection details. - - Args: - host (str): The Splunk server host. - port (int): The Splunk management port. - username (Optional[str]): Username for authentication. Defaults to None. - password (Optional[str]): Password for authentication. Defaults to None. - scheme (str): HTTP or HTTPS. Defaults to "https". - verify (bool): Whether to verify SSL certificates. Defaults to True. - timeout (int): Request timeout in seconds. Defaults to 30. - logger (Optional[Any]): Logger instance. If provided, actions will be logged. - """ - self.base_url = f"{scheme}://{host}:{port}" - self.auth = (username, password) - self.verify = verify - self.timeout = timeout - self.logger = logger or setup_logger(__name__) - - # Suppress InsecureRequestWarnings if the user sets verify=False - if not self.verify: - import urllib3 - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - def _log(self, msg: str, level: str = "info"): - if self.logger: - log_method = getattr(self.logger, level, self.logger.info) - log_method(msg) - - def query( - self, - search: str, - count: int = 1000, - earliest_time: Optional[str] = None, - latest_time: Optional[str] = None - ) -> Generator[Dict[str, Any], None, None]: - """ - Submit a search job to Splunk and stream results as dictionaries. - - Args: - search (str): The search query string. - count (int): Number of results per batch. Defaults to 1000. - earliest_time (Optional[str]): Earliest time for the search. Defaults to None. - latest_time (Optional[str]): Latest time for the search. Defaults to None. - - Yields: - Dict[str, Any]: Each search result as a dictionary. - - Logs actions if logger is enabled. - """ - # 1️⃣ Create job - self._log(f"Submitting search job: {search}") - data = { - "search": search, - "output_mode": "json", - "count": count - } - if earliest_time: - data["earliest_time"] = earliest_time - if latest_time: - data["latest_time"] = latest_time - - create_resp = httpx.post( - f"{self.base_url}/services/search/jobs", - auth=self.auth, - data=data, - verify=self.verify, - timeout=self.timeout - ) - create_resp.raise_for_status() - sid = create_resp.json()["sid"] - - # 2️⃣ Poll until ready - while True: - self._log(f"Polling job {sid} status...") - status_resp = httpx.get( - f"{self.base_url}/services/search/jobs/{sid}", - auth=self.auth, - params={"output_mode": "json"}, - verify=self.verify, - timeout=self.timeout - ) - status_resp.raise_for_status() - content = status_resp.json() - if content["entry"][0]["content"]["isDone"]: - break - - # 3️⃣ Fetch results - offset = 0 - while True: - self._log(f"Fetching results batch starting at offset {offset}") - results_resp = httpx.get( - f"{self.base_url}/services/search/jobs/{sid}/results", - auth=self.auth, - params={ - "output_mode": "json", - "count": count, - "offset": offset - }, - verify=self.verify, - timeout=self.timeout - ) - results_resp.raise_for_status() - results = results_resp.json().get("results", []) - if not results: - break - for row in results: - yield row - offset += len(results) diff --git a/ppp_connectors/helpers.py b/ppp_connectors/helpers.py deleted file mode 100644 index 3f014ef..0000000 --- a/ppp_connectors/helpers.py +++ /dev/null @@ -1,102 +0,0 @@ -from datetime import datetime -from dotenv import dotenv_values, find_dotenv -import logging -import os -import sys -from typing import Dict, Set, List, Any, Optional - - -def check_required_env_vars(config: Dict[str, str], required_vars: List[str]) -> None: - """Ensure that the env variables required for a function are present either in \ - the .env file, or in the system's environment variables. - - Args: - config (Dict[str, str]): the env_config variable that contains values from the .env file - required_vars (List[str]): the env variables required for a function to successfully function - """ - - dotenv_missing_vars: Set[str] = set(required_vars) - set(config.keys()) - osenv_missing_vars: Set[str] = set(required_vars) - set(os.environ) - missing_vars = dotenv_missing_vars | osenv_missing_vars - - if dotenv_missing_vars and osenv_missing_vars: - print(f'[!] Error: missing required environment variables: {", ".join(missing_vars)}. ' - 'Please ensure these are present either in your .env file, or in the ' - 'system\'s environment variables.', file=sys.stderr) - sys.exit(1) - - -def combine_env_configs() -> Dict[str, Any]: - """Find a .env file if it exists, and combine it with system environment - variables to form a "combined_config" dictionary of environment variables - - Returns: - Dict: a dictionary containing the output of a .env file (if found), and - system environment variables - """ - - env_config: Dict[str, Any] = dict(dotenv_values(find_dotenv())) - - combined_config: Dict[str, Any] = {**env_config, **dict(os.environ)} - - return combined_config - - -def validate_date_string(date_str: str) -> bool: - """Validates that a date string is, well, a valid date string - - Args: - date_str (str): a string in "YYYY-MM-DD" format - - Returns: - bool: True or False as valid or not - """ - try: - datetime.strptime(date_str, "%Y-%m-%d") - return True - except ValueError: - return False - - -def setup_logger( - name: str = __name__, - level: int = logging.INFO, - log_file: Optional[str] = None, - use_stdout: bool = True -) -> logging.Logger: - """ - Configures and returns a logger with optional StreamHandler and/or FileHandler. - - This function checks if a logger with the specified name already has any StreamHandler - or FileHandler attached, and if not, it adds them according to the parameters. - - Args: - name (str): The name of the logger to configure. - level (int): The logging level to set for the logger. Defaults to logging.INFO. - log_file (Optional[str]): If provided, logs will be written to this file. - use_stdout (bool): Whether to log to standard output. Defaults to True. - - Returns: - logging.Logger: A configured logger instance. - """ - logger: logging.Logger = logging.getLogger(name) - - logger.setLevel(level) - - formatter = logging.Formatter( - '[%(asctime)s]\t%(levelname)s\t%(name)s:%(lineno)d\t%(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - ) - - if use_stdout and not any(isinstance(h, logging.StreamHandler) for h in logger.handlers): - stream_handler = logging.StreamHandler(sys.stdout) - stream_handler.setFormatter(formatter) - logger.addHandler(stream_handler) - - if log_file and not any(isinstance(h, logging.FileHandler) for h in logger.handlers): - file_handler = logging.FileHandler(log_file) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - - logger.propagate = False - return logger diff --git a/ppp_connectors/tests/__init__.py b/ppp_connectors/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ppp_connectors/tests/conftest.py b/ppp_connectors/tests/conftest.py deleted file mode 100644 index b2b9b67..0000000 --- a/ppp_connectors/tests/conftest.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest -import vcr -from pathlib import Path - -AUTH_PARAM_REDACT = [ - # List of substrings that, if present in a key, will cause redaction (case-insensitive) - "key", "api_key", "access_token", "auth", "authorization", "user", "pass", "api", "x-api", "x_api" -] - - -# Redact sensitive values from request body parameters -import json -def redact_sensitive(request): - """Redact sensitive values from request body parameters.""" - if request.body: - try: - body = json.loads(request.body) - for k in list(body.keys()): - if any(redact_key.lower() in k.lower() for redact_key in AUTH_PARAM_REDACT): - body[k] = "REDACTED" - request.body = json.dumps(body) - except Exception: - pass # skip non-JSON or malformed bodies - - if request.headers: - for k in list(request.headers.keys()): - if any(redact_key.lower() in k.lower() for redact_key in AUTH_PARAM_REDACT): - request.headers[k] = "REDACTED" - - return request - - -@pytest.fixture -def vcr_cassette(request): - """Provides a VCR instance that stores cassettes in a test-local 'cassettes' folder.""" - test_dir = Path(request.module.__file__).parent - cassette_dir = test_dir / "cassettes" - - config = { - "cassette_library_dir": str(cassette_dir), - "path_transformer": vcr.VCR.ensure_suffix(".yaml"), - "record_mode": "once", - "filter_headers": [("Authorization", "DUMMY")], - "before_record": redact_sensitive - } - - return vcr.VCR(**config) \ No newline at end of file diff --git a/ppp_connectors/tests/test_broker/test_integration_broker.py b/ppp_connectors/tests/test_broker/test_integration_broker.py deleted file mode 100644 index 838ba8f..0000000 --- a/ppp_connectors/tests/test_broker/test_integration_broker.py +++ /dev/null @@ -1,14 +0,0 @@ -import httpx -from ppp_connectors.api_connectors.broker import Broker - -def test_integration_get_with_mock_transport(): - def handler(request): - assert request.url.path == "/hello" - return httpx.Response(200, json={"msg": "ok"}) - - transport = httpx.MockTransport(handler) - broker = Broker(base_url="https://testserver", timeout=5) - broker.session = httpx.Client(transport=transport) - response = broker.get("/hello") - assert response.status_code == 200 - assert response.json() == {"msg": "ok"} diff --git a/ppp_connectors/tests/test_broker/test_unit_asyncbroker.py b/ppp_connectors/tests/test_broker/test_unit_asyncbroker.py deleted file mode 100644 index ab1c6e5..0000000 --- a/ppp_connectors/tests/test_broker/test_unit_asyncbroker.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest -import httpx -from ppp_connectors.api_connectors.broker import AsyncBroker - - -def test_asyncbroker_rejects_mounts(): - """Ensure that AsyncBroker raises an error if 'mounts' is passed.""" - with pytest.raises(ValueError, match="mounts.*not supported"): - AsyncBroker(base_url="https://example.com", mounts={"http://": object()}) - - -def test_asyncbroker_explicit_proxy_used(mocker): - """Ensure that an explicitly provided proxy is passed to the AsyncClient.""" - client_mock = mocker.patch("httpx.AsyncClient") - AsyncBroker(base_url="https://example.com", proxy="http://explicit:1234") - client_mock.assert_called_once() - assert client_mock.call_args.kwargs["proxy"] == "http://explicit:1234" \ No newline at end of file diff --git a/ppp_connectors/tests/test_broker/test_unit_broker.py b/ppp_connectors/tests/test_broker/test_unit_broker.py deleted file mode 100644 index ba0887d..0000000 --- a/ppp_connectors/tests/test_broker/test_unit_broker.py +++ /dev/null @@ -1,67 +0,0 @@ -import pytest -from ppp_connectors.api_connectors.broker import Broker - -def test_get_calls_make_request(mocker): - broker = Broker(base_url="https://example.com") - mock_request = mocker.patch.object(broker, "_make_request") - broker.get("/test", params={"foo": "bar"}) - mock_request.assert_called_once_with("GET", "/test", params={"foo": "bar"}) - -def test_post_calls_make_request(mocker): - broker = Broker(base_url="https://example.com") - mock_request = mocker.patch.object(broker, "_make_request") - broker.post("/submit", json={"data": 123}) - mock_request.assert_called_once_with("POST", "/submit", json={"data": 123}) - -def test_logging_enabled_logs_message(mocker): - mock_logger = mocker.MagicMock() - mock_setup_logger = mocker.patch("ppp_connectors.api_connectors.broker.setup_logger", return_value=mock_logger) - broker = Broker(base_url="https://example.com", enable_logging=True) - broker._log("test message") - mock_logger.info.assert_called_once_with("test message") - -def test_env_only_proxy_from_dotenv(monkeypatch, mocker): - # Simulate .env via env_config when load_env_vars=True - mock_combine = mocker.patch( - "ppp_connectors.api_connectors.broker.combine_env_configs", - return_value={"HTTPS_PROXY": "http://env-proxy:8080"} - ) - broker = Broker(base_url="https://example.com", load_env_vars=True, trust_env=False) - assert broker.proxy == "http://env-proxy:8080" - assert broker.mounts is None - mock_combine.assert_called_once() - -def test_env_only_proxy_from_osenv(monkeypatch): - # Simulate OS env with trust_env=True - monkeypatch.setenv("HTTPS_PROXY", "http://os-proxy:9090") - broker = Broker(base_url="https://example.com", load_env_vars=False, trust_env=True) - assert broker.proxy == "http://os-proxy:9090" - assert broker.mounts is None - -def test_env_per_scheme_mounts(monkeypatch): - # Different HTTP and HTTPS proxies from env - monkeypatch.setenv("HTTP_PROXY", "http://http-proxy:8000") - monkeypatch.setenv("HTTPS_PROXY", "http://https-proxy:9000") - broker = Broker(base_url="https://example.com", load_env_vars=False, trust_env=True) - assert broker.proxy is None - assert "http://" in broker.mounts and "https://" in broker.mounts - -def test_priority_mounts_over_proxy(monkeypatch): - # If mounts are provided, they win - mounts = {"http://": object(), "https://": object()} - broker = Broker(base_url="https://example.com", mounts=mounts, proxy="http://should-not-use:1111") - assert broker.mounts == mounts - assert broker.proxy == "http://should-not-use:1111" # Stored but not used for session if mounts present - -def test_priority_proxy_over_env(monkeypatch): - monkeypatch.setenv("HTTPS_PROXY", "http://os-proxy:9999") - broker = Broker(base_url="https://example.com", proxy="http://explicit-proxy:7777", trust_env=True) - assert broker.proxy == "http://explicit-proxy:7777" - -def test_no_proxy_when_nothing_set(monkeypatch): - monkeypatch.delenv("ALL_PROXY", raising=False) - monkeypatch.delenv("HTTP_PROXY", raising=False) - monkeypatch.delenv("HTTPS_PROXY", raising=False) - broker = Broker(base_url="https://example.com", load_env_vars=False, trust_env=False) - assert broker.proxy is None - assert broker.mounts is None diff --git a/ppp_connectors/tests/test_elasticsearch/test_unit_elasticsearch.py b/ppp_connectors/tests/test_elasticsearch/test_unit_elasticsearch.py deleted file mode 100644 index 1ee0b07..0000000 --- a/ppp_connectors/tests/test_elasticsearch/test_unit_elasticsearch.py +++ /dev/null @@ -1,67 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -from ppp_connectors.dbms_connectors.elasticsearch import ElasticsearchConnector - -class TestElasticsearchConnector(unittest.TestCase): - - def setUp(self): - self.mock_logger = MagicMock() - self.connector = ElasticsearchConnector( - hosts=["http://localhost:9200"], - username="user", - password="pass", - logger=self.mock_logger - ) - - @patch("ppp_connectors.dbms_connectors.elasticsearch.Elasticsearch") - def test_initialization(self, mock_es): - ElasticsearchConnector( - hosts=["http://localhost:9200"], - username="user", - password="pass", - logger=self.mock_logger - ) - mock_es.assert_called_with(["http://localhost:9200"], basic_auth=("user", "pass")) - - @patch("ppp_connectors.dbms_connectors.elasticsearch.Elasticsearch") - def test_query_scroll(self, mock_es): - mock_client = MagicMock() - mock_client.search.return_value = { - "_scroll_id": "abc123", - "hits": {"hits": [{"_id": 1}, {"_id": 2}]} - } - mock_client.scroll.side_effect = [ - {"_scroll_id": "abc123", "hits": {"hits": [{"_id": 3}]}}, - {"_scroll_id": "abc123", "hits": {"hits": []}}, - ] - mock_es.return_value = mock_client - connector = ElasticsearchConnector( - hosts=["http://localhost:9200"], - username="user", - password="pass", - logger=self.mock_logger - ) - results = list(connector.query(index="test", query={"match_all": {}})) - self.assertEqual(len(results), 3) - self.assertEqual(results[0]["_id"], 1) - - @patch("ppp_connectors.dbms_connectors.elasticsearch.helpers.bulk") - def test_bulk_insert(self, mock_bulk): - mock_bulk.return_value = (3, []) - data = [{"_id": "1", "name": "Alice"}, {"_id": "2", "name": "Bob"}, {"_id": "3", "name": "Charlie"}] - success, errors = self.connector.bulk_insert(index="test-index", data=data) - self.assertEqual(success, 3) - self.assertEqual(errors, []) - self.mock_logger.info.assert_called_with("Bulk insert completed successfully") - - @patch("ppp_connectors.dbms_connectors.elasticsearch.helpers.bulk") - def test_bulk_insert_with_errors(self, mock_bulk): - mock_bulk.return_value = (2, [{"error": "failed to insert"}]) - data = [{"_id": "1", "name": "Alice"}, {"_id": "2", "name": "Bob"}] - success, errors = self.connector.bulk_insert(index="test-index", data=data) - self.assertEqual(success, 2) - self.assertTrue(errors) - self.mock_logger.error.assert_called() - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/ppp_connectors/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml b/ppp_connectors/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml deleted file mode 100644 index f98fe7e..0000000 --- a/ppp_connectors/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml +++ /dev/null @@ -1,66 +0,0 @@ -interactions: -- request: - body: '{"query": "dark web"}' - headers: - Authorization: - - DUMMY - accept: - - application/json - accept-encoding: - - gzip, deflate - connection: - - keep-alive - content-length: - - '20' - content-type: - - application/json - host: - - api.flashpoint.io - user-agent: - - python-httpx/0.28.1 - method: POST - uri: https://api.flashpoint.io/sources/v2/fraud - response: - body: - string: !!binary | - H4sIAAAAAAAAA6tWyixJzS1WsoqO1VEqzqxKVbIy0FEqyS9JzFGyqlYqS8wphQgVpeYklmTm5ylZ - Kdkq1dYCAEK/yh84AAAA - headers: - CF-RAY: - - 95d9b8290dfc137a-MEM - Connection: - - keep-alive - Content-Encoding: - - gzip - Content-Type: - - application/json - Date: - - Fri, 11 Jul 2025 16:36:22 GMT - Server: - - cloudflare - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Transfer-Encoding: - - chunked - cf-cache-status: - - DYNAMIC - ratelimit-limit: - - '5' - ratelimit-remaining: - - '4' - ratelimit-reset: - - '1' - via: - - kong/2.8.3 - x-kong-proxy-latency: - - '6' - x-kong-upstream-latency: - - '84' - x-ratelimit-limit-second: - - '5' - x-ratelimit-remaining-second: - - '4' - status: - code: 200 - message: OK -version: 1 diff --git a/ppp_connectors/tests/test_flashpoint/test_integration_flashpoint.py b/ppp_connectors/tests/test_flashpoint/test_integration_flashpoint.py deleted file mode 100644 index d2b0cc6..0000000 --- a/ppp_connectors/tests/test_flashpoint/test_integration_flashpoint.py +++ /dev/null @@ -1,11 +0,0 @@ -import httpx -import pytest -from ppp_connectors.api_connectors.flashpoint import FlashpointConnector - -@pytest.mark.integration -def test_flashpoint_search_fraud_vcr(vcr_cassette): - with vcr_cassette.use_cassette("test_flashpoint_search_fraud_vcr"): - connector = FlashpointConnector(load_env_vars=True) - result = connector.search_fraud("dark web") - assert isinstance(result, httpx.Response) - assert "items" in result.json() \ No newline at end of file diff --git a/ppp_connectors/tests/test_flashpoint/test_unit_async_flashpoint.py b/ppp_connectors/tests/test_flashpoint/test_unit_async_flashpoint.py deleted file mode 100644 index b7182ed..0000000 --- a/ppp_connectors/tests/test_flashpoint/test_unit_async_flashpoint.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -import httpx -from unittest.mock import patch, AsyncMock -from ppp_connectors.api_connectors.flashpoint import AsyncFlashpointConnector - - -@pytest.mark.asyncio -async def test_async_init_with_api_key(): - connector = AsyncFlashpointConnector(api_key="test_token") - assert connector.api_key == "test_token" - assert connector.headers["Authorization"] == "Bearer test_token" - - -@patch.dict("os.environ", {"FLASHPOINT_API_KEY": "env_token"}, clear=True) -@pytest.mark.asyncio -async def test_async_init_with_env_key(): - connector = AsyncFlashpointConnector(load_env_vars=True) - assert connector.api_key == "env_token" - assert connector.headers["Authorization"] == "Bearer env_token" - - -@pytest.mark.asyncio -async def test_async_init_missing_key(): - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(ValueError, match="FLASHPOINT_API_KEY is required"): - AsyncFlashpointConnector() - - -@patch("ppp_connectors.api_connectors.flashpoint.AsyncFlashpointConnector.post", new_callable=AsyncMock) -@pytest.mark.asyncio -async def test_async_search_fraud(mock_post): - import json - - request = httpx.Request("POST", "https://api.flashpoint.io/mock") - payload = {"success": True, "data": []} - mock_response = httpx.Response( - 200, - request=request, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_post.return_value = mock_response - - connector = AsyncFlashpointConnector(api_key="mock_token") - result = await connector.search_fraud("credential stuffing") - - assert isinstance(result, httpx.Response) - assert result.json() == payload - mock_post.assert_awaited_once() \ No newline at end of file diff --git a/ppp_connectors/tests/test_flashpoint/test_unit_flashpoint.py b/ppp_connectors/tests/test_flashpoint/test_unit_flashpoint.py deleted file mode 100644 index f2a2501..0000000 --- a/ppp_connectors/tests/test_flashpoint/test_unit_flashpoint.py +++ /dev/null @@ -1,45 +0,0 @@ -import httpx -import pytest -from unittest.mock import patch, MagicMock -from ppp_connectors.api_connectors.flashpoint import FlashpointConnector - -def test_init_with_api_key(): - connector = FlashpointConnector(api_key="test_token") - assert connector.api_key == "test_token" - assert connector.headers["Authorization"] == "Bearer test_token" - - -@patch.dict("os.environ", {"FLASHPOINT_API_KEY": "env_token"}, clear=True) -def test_init_with_env_key(): - connector = FlashpointConnector(load_env_vars=True) - assert connector.api_key == "env_token" - assert connector.headers["Authorization"] == "Bearer env_token" - - -def test_init_missing_key(): - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(ValueError, match="FLASHPOINT_API_KEY is required"): - FlashpointConnector() - - -@patch("ppp_connectors.api_connectors.flashpoint.FlashpointConnector.post") -def test_search_fraud(mock_post): - import json - - # Use a real httpx.Response so our test matches the new return type - request = httpx.Request("POST", "https://api.flashpoint.io/mock") - payload = {"success": True, "data": []} - mock_response = httpx.Response( - 200, - request=request, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_post.return_value = mock_response - - connector = FlashpointConnector(api_key="mock_token") - result = connector.search_fraud("credential stuffing") - - assert isinstance(result, httpx.Response) - assert result.json() == payload - mock_post.assert_called_once() \ No newline at end of file diff --git a/ppp_connectors/tests/test_generic/cassettes/test_generic_get_github_api.yaml b/ppp_connectors/tests/test_generic/cassettes/test_generic_get_github_api.yaml deleted file mode 100644 index 825a3d7..0000000 --- a/ppp_connectors/tests/test_generic/cassettes/test_generic_get_github_api.yaml +++ /dev/null @@ -1,87 +0,0 @@ -interactions: -- request: - body: '' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - connection: - - keep-alive - host: - - api.github.com - user-agent: - - python-httpx/0.28.1 - method: GET - uri: https://api.github.com/ - response: - body: - string: !!binary | - H4sIAAAAAAAAA5VWy27jMAy85yuCHHoKqnuAop9iKDZja1e2XIlukQr591KisrHdbWVd8gBmxOGQ - Gtvv9vtDPVkLA1aTA1tNVh9O+0OHOLqTEHJUz63Cbjo/16YXAXI4fiPJCTtj1adEZQZXddjr9UGz - QxwgqqF1ojbDAHXkUKFRq5oP8KLWKihSzY2rrQr8LnIJTnJNA5UDaeturWzVIoNIWwOvby/+bQJ7 - vfmnUbZwHMmg+MMZi0djG7BJIJnTKyysECiuoAj0UmmX0R9GJBjJrUNv/qgci0GJ8E7eZwkRxIQL - QJPDR0yCG63NB9gcJXZyuYPnXNqeLS4wl8BeoLQtYBpWq1y2v4jxInw91pCuQaYuIViocm4q27jI - KFkHJmQEMYg1/YXrJs8DjhlankEXbXVkzLp4sjAap9DYK/n44hd/1/cqzWcwqC73MMj0t8CyaGNb - OaQwyrAJ6oSnz1R5Qf0nVUHOtsc5IrL8K15HyotvofG/OgiyLygQ4dzpOJ0pNKvN+yyYwGQrESqt - Qmz9nqcPYCI+JpohBiDZ+zFQToo4+tT/bAtKQnk+kYLcXDzgCqYaA2jTPB3li4UmY2Q8L0G9YFt8 - LJBsuR+zfaCJwYNBM9I2lBgaGT9nTpK18cWARh2Qc9L8PuVWPLoTLhI3E4sWDuuuYNvQYoUStwLh - Z7MWLwW72+4L1rGgQFwJAAA= - headers: - Accept-Ranges: - - bytes - Access-Control-Allow-Origin: - - '*' - Access-Control-Expose-Headers: - - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, - X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, - X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, - X-GitHub-Request-Id, Deprecation, Sunset - Cache-Control: - - public, max-age=60, s-maxage=60 - Content-Encoding: - - gzip - Content-Length: - - '530' - Content-Security-Policy: - - default-src 'none' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 11 Jul 2025 16:41:44 GMT - ETag: - - '"4f825cc84e1c733059d46e76e6df9db557ae5254f9625dfe8e1b09499c449438"' - Referrer-Policy: - - origin-when-cross-origin, strict-origin-when-cross-origin - Server: - - github.com - Strict-Transport-Security: - - max-age=31536000; includeSubdomains; preload - Vary: - - Accept,Accept-Encoding, Accept, X-Requested-With - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - deny - X-GitHub-Media-Type: - - github.v3; format=json - X-GitHub-Request-Id: - - CBC0:27B4C:44C024C:8AF37AD:68713ED1 - X-RateLimit-Limit: - - '60' - X-RateLimit-Remaining: - - '59' - X-RateLimit-Reset: - - '1752255713' - X-RateLimit-Resource: - - core - X-RateLimit-Used: - - '1' - X-XSS-Protection: - - '0' - x-github-api-version-selected: - - '2022-11-28' - status: - code: 200 - message: OK -version: 1 diff --git a/ppp_connectors/tests/test_generic/test_integration_generic_connector.py b/ppp_connectors/tests/test_generic/test_integration_generic_connector.py deleted file mode 100644 index c473169..0000000 --- a/ppp_connectors/tests/test_generic/test_integration_generic_connector.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest -import vcr -from ppp_connectors.api_connectors.generic import GenericConnector - -@pytest.mark.integration -def test_generic_get_github_api(vcr_cassette): - with vcr_cassette.use_cassette("test_generic_get_github_api"): - connector = GenericConnector(base_url="https://api.github.com") - response = connector.get("/") - - data = response.json() - assert "current_user_url" in data diff --git a/ppp_connectors/tests/test_generic/test_unit_async_generic_connector.py b/ppp_connectors/tests/test_generic/test_unit_async_generic_connector.py deleted file mode 100644 index 80410e5..0000000 --- a/ppp_connectors/tests/test_generic/test_unit_async_generic_connector.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest -import httpx -from unittest.mock import AsyncMock, patch -from ppp_connectors.api_connectors.generic import AsyncGenericConnector - - -@pytest.mark.asyncio -async def test_async_init_sets_base_url(): - connector = AsyncGenericConnector(base_url="https://example.com") - assert connector.base_url == "https://example.com" - - -@patch("ppp_connectors.api_connectors.generic.AsyncGenericConnector._make_request", new_callable=AsyncMock) -@pytest.mark.asyncio -async def test_async_get_request(mock_make_request): - mock_response = httpx.Response(200, json={"result": "ok"}) - mock_make_request.return_value = mock_response - - connector = AsyncGenericConnector(base_url="https://example.com") - response = await connector.request("GET", "/test") - - assert response.status_code == 200 - assert response.json() == {"result": "ok"} - mock_make_request.assert_awaited_once_with( - method="GET", - endpoint="/test", - headers=connector.headers, - params=None, - json=None, - auth=None, - retry_kwargs=None, - ) - - -@patch("ppp_connectors.api_connectors.generic.AsyncGenericConnector._make_request", new_callable=AsyncMock) -@pytest.mark.asyncio -async def test_async_post_request(mock_make_request): - mock_response = httpx.Response(200, json={"posted": True}) - mock_make_request.return_value = mock_response - - connector = AsyncGenericConnector(base_url="https://example.com") - response = await connector.request("POST", "/submit", json={"key": "value"}) - - assert response.status_code == 200 - assert response.json() == {"posted": True} - mock_make_request.assert_awaited_once_with( - method="POST", - endpoint="/submit", - headers=connector.headers, - params=None, - json={"key": "value"}, - auth=None, - retry_kwargs=None, - ) \ No newline at end of file diff --git a/ppp_connectors/tests/test_generic/test_unit_generic_connector.py b/ppp_connectors/tests/test_generic/test_unit_generic_connector.py deleted file mode 100644 index 299cd9a..0000000 --- a/ppp_connectors/tests/test_generic/test_unit_generic_connector.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest -from unittest.mock import patch, MagicMock -from ppp_connectors.api_connectors.generic import GenericConnector - - -def test_init_sets_base_url(): - connector = GenericConnector(base_url="https://example.com") - assert connector.base_url == "https://example.com" - - -@patch("ppp_connectors.api_connectors.generic.GenericConnector._make_request") -def test_get_request(mock_make_request): - mock_response = MagicMock() - mock_response.json.return_value = {"result": "ok"} - mock_make_request.return_value = mock_response - - connector = GenericConnector(base_url="https://example.com") - response = connector.get("/test") - - assert response.json() == {"result": "ok"} - mock_make_request.assert_called_once_with("GET", "/test", params=None) - - -@patch("ppp_connectors.api_connectors.generic.GenericConnector._make_request") -def test_post_request(mock_make_request): - mock_response = MagicMock() - mock_response.json.return_value = {"posted": True} - mock_make_request.return_value = mock_response - - connector = GenericConnector(base_url="https://example.com") - response = connector.post("/submit", json={"key": "value"}) - - assert response.json() == {"posted": True} - mock_make_request.assert_called_once_with("POST", "/submit", json={"key": "value"}) diff --git a/ppp_connectors/tests/test_ipqs/__init__.py b/ppp_connectors/tests/test_ipqs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ppp_connectors/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml b/ppp_connectors/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml deleted file mode 100644 index 85fb2e0..0000000 --- a/ppp_connectors/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml +++ /dev/null @@ -1,64 +0,0 @@ -interactions: -- request: - body: '{"url": "github.com", "key": "REDACTED"}' - headers: - accept: - - '*/*' - accept-encoding: - - gzip, deflate - connection: - - keep-alive - content-length: - - '61' - content-type: - - application/json - host: - - ipqualityscore.com - user-agent: - - python-httpx/0.28.1 - method: POST - uri: https://ipqualityscore.com/api/json/url/ - response: - body: - string: !!binary | - H4sIAAAAAAAAA3RUTW/bMAz9KwIPOzmu7DhN4mGXDcO2yy7dDtsaGIyt2EJkyROptlnR/z6ocT76 - dbL4HkXykbTuoVdE2Coo4SrUtSJKIQHaH6FkH1QCwRJuFJQbNKQSaFyP2kIJreYurNPa9ZCAd46r - 1yk9VNg0/jEiZIVMF3maZUVaxFTK3yj//EbtLCvLFe+GWBqrO76+6Lg370XdoSfFHwJvJosYgJED - VbVrFJS5lAkM2KqK9L9oL2bLy+Wh5Mqj3UI5nSfQWKpu0OjmoHFAv9W2PYqkAfv+HOjR3KI/dWHo - NHVPbgQadK1doCOETTB8tLymbUW1i0Fk1Bgs+91YOfy8ggQM2jbE8kfw8/fYDGTVOr+DEj65fgis - PIl34ptl5a1iOMp7nOM9dKHHOINsIXYKPQlsHSTAulfE2A9QZtkyW84KOZMJaHJQQi7lfJLJiVz+ - yIoyl+VMTmRRSgkPx/DsA3GcRvyqJs5cNdqrOhpjG6lzniuj7bY6kEf9nYvXqnG2p0ZGvazZRMFf - NH8NayE+Bm0agbYR1OlBkNtw7L5wVqAgbVujElE7Y3DtPLK+UWIwyBvneyH2QWDf8V3Fpjkb66by - qnb+BDU9+voI7mWwqjvrjGu1Iij/rBLA0SOaT3d4lUB/d86i4WmKNPR3qUlb51qjxrVGw8WbTPYm - k7/BvARXCVg6r6WxlKeDXKSWnFXpuC6Wpq+BxQvQ0iTLF9MUb6mxNJnmqfPtiM/l/IDnWVq7NGz3 - TJFnB2KWj7U+nuUBltkpafYs6SoBqtFa1VTBGyihYx7K64vriycvxEZbNGce9NLFq79BEVfxJ4df - 2/Vlq3/fhgwe/gMAAP//AwB3HyEu9gQAAA== - headers: - CF-RAY: - - 95d29762d81310be-ORD - Connection: - - keep-alive - Content-Encoding: - - gzip - Content-Type: - - application/json; charset=UTF-8 - Date: - - Thu, 10 Jul 2025 19:50:40 GMT - NEL: - - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' - Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=cnp43vf6ymYDAXC9fg%2FhY3TA2duBQq5GEDBoldh1PtroZ77jm2tQbOO7z5x%2Ffig70pMLaIIJmjnULNTy5Ts1IGQe4CgYtA244Lz7XwHYtCbbyHWOXBYB72ebfp50XXiPyMMb9g%3D%3D"}],"group":"cf-nel","max_age":604800}' - Server: - - cloudflare - Transfer-Encoding: - - chunked - alt-svc: - - h3=":443"; ma=86400 - cf-cache-status: - - DYNAMIC - server-timing: - - cfL4;desc="?proto=TCP&rtt=35415&min_rtt=33540&rtt_var=13917&sent=5&recv=9&lost=0&retrans=0&sent_bytes=3948&recv_bytes=1937&delivery_rate=129516&cwnd=38&unsent_bytes=0&cid=ff31fc6046b459f4&ts=565&x=0" - status: - code: 200 - message: OK -version: 1 diff --git a/ppp_connectors/tests/test_ipqs/test_integration_ipqs.py b/ppp_connectors/tests/test_ipqs/test_integration_ipqs.py deleted file mode 100644 index 7bd3abd..0000000 --- a/ppp_connectors/tests/test_ipqs/test_integration_ipqs.py +++ /dev/null @@ -1,13 +0,0 @@ -import httpx -import pytest -from ppp_connectors.api_connectors.ipqs import IPQSConnector - -@pytest.mark.integration -def test_ipqs_malicious_url_vcr(vcr_cassette): - with vcr_cassette.use_cassette("test_ipqs_malicious_url_vcr"): - connector = IPQSConnector(load_env_vars=True, enable_logging=True) - result = connector.malicious_url("github.com") - - assert isinstance(result, httpx.Response) - assert "domain" in result.json() - assert result.json()["domain"] == "github.com" \ No newline at end of file diff --git a/ppp_connectors/tests/test_ipqs/test_unit_async_ipqs.py b/ppp_connectors/tests/test_ipqs/test_unit_async_ipqs.py deleted file mode 100644 index 2b32e2f..0000000 --- a/ppp_connectors/tests/test_ipqs/test_unit_async_ipqs.py +++ /dev/null @@ -1,53 +0,0 @@ -import pytest -import httpx -from unittest.mock import patch, AsyncMock -from ppp_connectors.api_connectors.ipqs import AsyncIPQSConnector - - -@pytest.mark.asyncio -async def test_async_init_with_api_key(): - connector = AsyncIPQSConnector(api_key="test_key") - assert connector.api_key == "test_key" - assert connector.headers["Content-Type"] == "application/json" - - -@pytest.mark.asyncio -async def test_async_init_with_env_key(): - with patch.dict("os.environ", {"IPQS_API_KEY": "env_key"}): - connector = AsyncIPQSConnector(load_env_vars=True) - assert connector.api_key == "env_key" - assert connector.headers["Content-Type"] == "application/json" - - -@pytest.mark.asyncio -async def test_async_init_missing_key(): - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(ValueError, match="API key is required"): - AsyncIPQSConnector() - - -@patch("ppp_connectors.api_connectors.ipqs.AsyncIPQSConnector.post", new_callable=AsyncMock) -@pytest.mark.asyncio -async def test_async_malicious_url(mock_post): - import json - - request = httpx.Request("POST", "https://ipqualityscore.com/api/json/url/") - payload = {"success": True, "domain": "example.com"} - mock_response = httpx.Response( - 200, - request=request, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_post.return_value = mock_response - - connector = AsyncIPQSConnector(api_key="test_key") - response = await connector.malicious_url("example.com", strictness=1) - - assert isinstance(response, httpx.Response) - assert response.status_code == 200 - assert response.json() == payload - mock_post.assert_awaited_once_with( - "/url/", - json={"url": "example.com", "key": "test_key", "strictness": 1} - ) \ No newline at end of file diff --git a/ppp_connectors/tests/test_ipqs/test_unit_ipqs.py b/ppp_connectors/tests/test_ipqs/test_unit_ipqs.py deleted file mode 100644 index cda66fc..0000000 --- a/ppp_connectors/tests/test_ipqs/test_unit_ipqs.py +++ /dev/null @@ -1,45 +0,0 @@ -import httpx -import pytest -from unittest.mock import patch, MagicMock -from ppp_connectors.api_connectors.ipqs import IPQSConnector - - -def test_init_with_api_key(): - connector = IPQSConnector(api_key="test_key") - assert connector.api_key == "test_key" - assert connector.headers["Content-Type"] == "application/json" - - -def test_init_with_env_key(): - with patch.dict("os.environ", {"IPQS_API_KEY": "env_key"}): - connector = IPQSConnector(load_env_vars=True) - assert connector.api_key == "env_key" - - -def test_init_missing_key(): - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(ValueError, match="API key is required"): - IPQSConnector() - - -@patch("ppp_connectors.api_connectors.ipqs.IPQSConnector.post") -def test_malicious_url(mock_post): - # Build a real httpx.Response to match the new return type - import json - - request = httpx.Request("POST", "https://ipqualityscore.com/api/json/url/") - payload = {"success": True, "domain": "example.com"} - mock_response = httpx.Response( - 200, - request=request, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_post.return_value = mock_response - - connector = IPQSConnector(api_key="test_key") - result = connector.malicious_url("example.com") - - mock_post.assert_called_once() - assert isinstance(result, httpx.Response) - assert result.json() == payload diff --git a/ppp_connectors/tests/test_mongodb/test_unit_async_mongo.py b/ppp_connectors/tests/test_mongodb/test_unit_async_mongo.py deleted file mode 100644 index 6bbc7bf..0000000 --- a/ppp_connectors/tests/test_mongodb/test_unit_async_mongo.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -import types -import pytest -from unittest.mock import AsyncMock - -from ppp_connectors.dbms_connectors.mongo_async import AsyncMongoConnector - - -@pytest.mark.asyncio -async def test_async_init_and_ping(monkeypatch): - fake_client = types.SimpleNamespace() - fake_client.admin = types.SimpleNamespace() - fake_client.admin.command = AsyncMock(return_value={"ok": 1}) - - # Provide pymongo.asyncio.MongoClient for import inside the connector - ns = types.SimpleNamespace(AsyncMongoClient=lambda *a, **k: fake_client) - monkeypatch.setitem(sys.modules, "pymongo.asynchronous.mongo_client", ns) - - async with AsyncMongoConnector(uri="mongodb://localhost:27017") as conn: - assert isinstance(conn, AsyncMongoConnector) - - fake_client.admin.command.assert_awaited_once_with("ping") - - -class _AsyncCursor: - def __init__(self, items): - self._items = items - self._idx = 0 - - def batch_size(self, n): - return self - - def __aiter__(self): - self._idx = 0 - return self - - async def __anext__(self): - if self._idx >= len(self._items): - raise StopAsyncIteration - item = self._items[self._idx] - self._idx += 1 - return item - - -@pytest.mark.asyncio -async def test_async_find(monkeypatch): - docs = [{"_id": 1}, {"_id": 2}] - - class _FakeCollection: - def find(self, f, p): - return _AsyncCursor(docs) - - class _FakeDB(dict): - def __getitem__(self, k): - return _FakeCollection() - - class _FakeClient: - def __init__(self): - self.admin = types.SimpleNamespace() - self.admin.command = AsyncMock(return_value={"ok": 1}) - - def __getitem__(self, k): - return _FakeDB() - - def close(self): - pass - - fake_client = _FakeClient() - ns = types.SimpleNamespace(AsyncMongoClient=lambda *a, **k: fake_client) - monkeypatch.setitem(sys.modules, "pymongo.asynchronous.mongo_client", ns) - - async with AsyncMongoConnector(uri="mongodb://localhost:27017") as conn: - out = [doc async for doc in conn.find("db", "col", filter={})] - - assert out == docs - - -@pytest.mark.asyncio -async def test_async_aggregate(monkeypatch): - docs = [{"_id": 1, "count": 2}, {"_id": 2, "count": 3}] - - class _FakeCollection: - def aggregate(self, pipeline, **kwargs): - return _AsyncCursor(docs) - - class _FakeDB(dict): - def __getitem__(self, k): - return _FakeCollection() - - class _FakeClient: - def __init__(self): - self.admin = types.SimpleNamespace() - self.admin.command = AsyncMock(return_value={"ok": 1}) - - def __getitem__(self, k): - return _FakeDB() - - def close(self): - pass - - fake_client = _FakeClient() - ns = types.SimpleNamespace(AsyncMongoClient=lambda *a, **k: fake_client) - monkeypatch.setitem(sys.modules, "pymongo.asynchronous.mongo_client", ns) - - out = [] - async with AsyncMongoConnector(uri="mongodb://localhost:27017") as conn: - out = [doc async for doc in conn.aggregate("db", "col", pipeline=[{"$match": {}}], batch_size=50)] - - assert out == docs diff --git a/ppp_connectors/tests/test_mongodb/test_unit_mongo.py b/ppp_connectors/tests/test_mongodb/test_unit_mongo.py deleted file mode 100644 index 0ede1b1..0000000 --- a/ppp_connectors/tests/test_mongodb/test_unit_mongo.py +++ /dev/null @@ -1,219 +0,0 @@ -import pytest -from pymongo import UpdateOne -from pymongo.errors import ServerSelectionTimeoutError -from ppp_connectors.dbms_connectors.mongo import MongoConnector -from unittest.mock import patch, MagicMock - - -def test_mongo_query(monkeypatch): - # Prevent real connection by mocking MongoClient and returning a successful ping - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - mock_cursor = [ - {"_id": 1, "name": "Alice"}, - {"_id": 2, "name": "Bob"} - ] - mock_collection = MagicMock() - mock_collection.find.return_value.batch_size.return_value = mock_cursor - mock_db = {"test_collection": mock_collection} - mock_client = {"test_db": mock_db} - - connector = MongoConnector( - uri="mongodb://localhost:27017", - username="fake", - password="fake", - ) - connector.client = mock_client - - results = list(connector.query("test_db", "test_collection", {})) - assert results == mock_cursor - - -def test_insert_many(monkeypatch): - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - mock_insert_many = MagicMock() - mock_collection = {"insert_many": mock_insert_many} - mock_db = {"test_collection": MagicMock(return_value=mock_insert_many)} - mock_client = {"test_db": mock_db} - - connector = MongoConnector( - uri="mongodb://localhost:27017", - username="fake", - password="fake", - ) - connector.client = MagicMock() - connector.client.__getitem__.return_value.__getitem__.return_value.insert_many = mock_insert_many - - test_data = [{"_id": 1}, {"_id": 2}] - connector.insert_many("test_db", "test_collection", test_data) - - mock_insert_many.assert_called_once_with(test_data, ordered=False) - - -def test_distinct(monkeypatch): - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - mock_collection = MagicMock() - mock_collection.distinct.return_value = ["A", "B"] - connector = MongoConnector( - uri="mongodb://localhost:27017", - username="fake", - password="fake", - ssl=False - ) - connector.client = MagicMock() - connector.client.__getitem__.return_value.__getitem__.return_value = mock_collection - - values = connector.distinct("test_db", "test_collection", key="field", filter={"x": 1}, maxTimeMS=5000) - assert values == ["A", "B"] - mock_collection.distinct.assert_called_once_with("field", {"x": 1}, maxTimeMS=5000) - - -def test_aggregate(monkeypatch): - mock_cursor = [ - {"_id": 1, "count": 10}, - {"_id": 2, "count": 5}, - ] - mock_collection = MagicMock() - # aggregate().batch_size() returns an iterable cursor - mock_collection.aggregate.return_value.batch_size.return_value = mock_cursor - - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - fake_client.__getitem__.return_value.__getitem__.return_value = mock_collection - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - - connector = MongoConnector(uri="mongodb://localhost:27017") - pipeline = [{"$match": {"x": {"$gt": 1}}}, {"$group": {"_id": "$x", "count": {"$sum": 1}}}] - out = list(connector.aggregate("db", "col", pipeline, batch_size=100, allowDiskUse=True)) - assert out == mock_cursor - mock_collection.aggregate.assert_called_once_with(pipeline, allowDiskUse=True) - - -def test_mongo_connection_failure(): - with pytest.raises(ServerSelectionTimeoutError): - MongoConnector( - uri="mongodb://localhost:27018", # invalid port - username="fake", - password="fake", - timeout=1, - ) - - - -@patch("ppp_connectors.dbms_connectors.mongo.MongoClient") -def test_mongo_init_with_auth_and_ssl(mock_mongo_client): - # Mock ping success on the returned client instance - instance = MagicMock() - instance.admin.command.return_value = {"ok": 1} - mock_mongo_client.return_value = instance - - MongoConnector( - uri="mongodb://example.com:27017", - username="user", - password="pass", - auth_source="authdb", - auth_mechanism="SCRAM-SHA-1", - ssl=True - ) - mock_mongo_client.assert_called_once_with( - "mongodb://example.com:27017", - username="user", - password="pass", - authSource="authdb", - authMechanism="SCRAM-SHA-1", - ssl=True, - serverSelectionTimeoutMS=10000 - ) - - -@patch("ppp_connectors.dbms_connectors.mongo.MongoClient") -def test_mongo_init_defaults(mock_mongo_client): - instance = MagicMock() - instance.admin.command.return_value = {"ok": 1} - mock_mongo_client.return_value = instance - MongoConnector(uri="mongodb://localhost:27017") - mock_mongo_client.assert_called_once() - - - -def test_upsert_many_with_unique_key(monkeypatch): - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - mock_bulk_write = MagicMock() - mock_collection = MagicMock() - mock_collection.bulk_write = mock_bulk_write - - connector = MongoConnector( - uri="mongodb://localhost:27017", - username="fake", - password="fake", - ) - connector.client = MagicMock() - connector.client.__getitem__.return_value.__getitem__.return_value = mock_collection - - data = [{"_id": 1, "value": "A"}, {"_id": 2, "value": "B"}] - connector.upsert_many("test_db", "test_collection", data, unique_key="_id") - - ops = [ - UpdateOne({"_id": 1}, {"$set": {"_id": 1, "value": "A"}}, upsert=True), - UpdateOne({"_id": 2}, {"$set": {"_id": 2, "value": "B"}}, upsert=True) - ] - mock_bulk_write.assert_called_once_with(ops, ordered=False) - - -def test_upsert_many_raises_without_unique_key(monkeypatch): - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - connector = MongoConnector( - uri="mongodb://localhost:27017", - username="fake", - password="fake", - ) - mock_collection = MagicMock() - mock_collection.bulk_write = MagicMock() - connector.client = MagicMock() - connector.client.__getitem__.return_value.__getitem__.return_value = mock_collection - - data = [{"_id": i} for i in range(10)] - with pytest.raises(ValueError, match="unique_key must be provided for upsert_many"): - connector.upsert_many("test_db", "test_collection", data, unique_key=None) - - -def test_context_manager_closes_client(monkeypatch): - fake_client = MagicMock() - fake_client.admin.command.return_value = {"ok": 1} - monkeypatch.setattr( - "ppp_connectors.dbms_connectors.mongo.MongoClient", - MagicMock(return_value=fake_client), - ) - - with MongoConnector(uri="mongodb://localhost:27017") as conn: - assert isinstance(conn, MongoConnector) - - # After exiting context, close should be called - fake_client.close.assert_called_once() diff --git a/ppp_connectors/tests/test_odbc/test_unit_odbc.py b/ppp_connectors/tests/test_odbc/test_unit_odbc.py deleted file mode 100644 index d5d8f98..0000000 --- a/ppp_connectors/tests/test_odbc/test_unit_odbc.py +++ /dev/null @@ -1,82 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest - -import ppp_connectors.dbms_connectors.odbc as odbc_module -from ppp_connectors.dbms_connectors.odbc import ODBCConnector - - -@pytest.fixture -def mock_pyodbc(): - with patch("ppp_connectors.dbms_connectors.odbc._get_pyodbc") as mock_loader: - pyodbc_mock = MagicMock() - mock_loader.return_value = pyodbc_mock - yield pyodbc_mock - - -def test_odbcconnector_init(mock_pyodbc): - mock_logger = MagicMock() - connector = ODBCConnector("DSN=testdb", logger=mock_logger) - mock_pyodbc.connect.assert_called_once_with("DSN=testdb") - assert connector.logger == mock_logger - - -def test_odbcconnector_query_returns_rows(mock_pyodbc): - """Test that query returns rows as dictionaries.""" - mock_cursor = MagicMock() - mock_cursor.description = [("id",), ("name",)] - mock_cursor.fetchall.return_value = [(1, "Alice"), (2, "Bob")] - mock_pyodbc.connect.return_value.cursor.return_value = mock_cursor - connector = ODBCConnector("DSN=testdb") - - results = list(connector.query("SELECT * FROM users")) - - assert results == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] - - -def test_odbcconnector_bulk_insert(mock_pyodbc): - mock_cursor = MagicMock() - mock_pyodbc.connect.return_value.cursor.return_value = mock_cursor - connector = ODBCConnector("DSN=testdb") - - data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] - connector.bulk_insert("users", data) - - assert mock_cursor.executemany.called - assert mock_pyodbc.connect.return_value.commit.called - - -def test_odbcconnector_bulk_insert_empty_data(mock_pyodbc): - mock_cursor = MagicMock() - mock_pyodbc.connect.return_value.cursor.return_value = mock_cursor - connector = ODBCConnector("DSN=testdb") - - connector.bulk_insert("users", []) - - mock_cursor.executemany.assert_not_called() - - -def test_odbcconnector_context_manager_closes_connection(mock_pyodbc): - """Test that the context manager closes the connection.""" - mock_conn = MagicMock() - mock_pyodbc.connect.return_value = mock_conn - - with ODBCConnector("DSN=testdb") as connector: - assert isinstance(connector, ODBCConnector) - - mock_conn.close.assert_called_once() - - -def test_odbcconnector_raises_helpful_error_when_pyodbc_missing(monkeypatch): - """Ensure that using the connector without the extra raises a clear error.""" - monkeypatch.setattr(odbc_module, "_PYODBC_MODULE", None) - monkeypatch.setattr( - odbc_module, - "import_module", - MagicMock(side_effect=ImportError("pyodbc missing")), - ) - - with pytest.raises(ImportError) as excinfo: - ODBCConnector("DSN=testdb") - - assert "pyodbc is not installed" in str(excinfo.value) diff --git a/ppp_connectors/tests/test_splunk/test_unit_splunk.py b/ppp_connectors/tests/test_splunk/test_unit_splunk.py deleted file mode 100644 index fcd148c..0000000 --- a/ppp_connectors/tests/test_splunk/test_unit_splunk.py +++ /dev/null @@ -1,56 +0,0 @@ -import pytest -import httpx -from unittest.mock import patch, MagicMock -from ppp_connectors.dbms_connectors.splunk import SplunkConnector - - -@pytest.fixture -def connector(): - return SplunkConnector( - host="localhost", - port=8089, - username="admin", - password="admin123", - verify=False - ) - - -@patch("httpx.post") -@patch("httpx.get") -def test_query_success(mock_get, mock_post, connector): - # Mock POST /services/search/jobs - post_response = MagicMock() - post_response.raise_for_status = MagicMock() - post_response.json.return_value = {"sid": "abc123"} - mock_post.return_value = post_response - - # Mock GET /services/search/jobs/{sid} - get_status_response = MagicMock() - get_status_response.raise_for_status = MagicMock() - get_status_response.json.return_value = { - "entry": [{"content": {"isDone": True}}] - } - - # Mock GET /services/search/jobs/{sid}/results - get_results_response = MagicMock() - get_results_response.raise_for_status = MagicMock() - get_results_response.json.side_effect = [ - {"results": [{"foo": "bar"}, {"baz": "qux"}]}, - {"results": []} - ] - mock_get.side_effect = [get_status_response, get_results_response, get_results_response] - - results = list(connector.query("search index=_internal | head 2")) - assert len(results) == 2 - assert results[0]["foo"] == "bar" - assert results[1]["baz"] == "qux" - - -@patch("httpx.post") -def test_query_auth_error(mock_post, connector): - mock_response = MagicMock() - mock_response.raise_for_status.side_effect = httpx.HTTPError("401 Unauthorized") - mock_post.return_value = mock_response - - with pytest.raises(httpx.HTTPError): - list(connector.query("search index=_internal | head 1")) diff --git a/ppp_connectors/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml b/ppp_connectors/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml deleted file mode 100644 index 11230e7..0000000 --- a/ppp_connectors/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml +++ /dev/null @@ -1,1870 +0,0 @@ -interactions: -- request: - body: '' - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - connection: - - keep-alive - host: - - api.spycloud.io - user-agent: - - REDACTED - x-api-key: - - REDACTED - method: GET - uri: https://api.spycloud.io/sp-v2/breach/data/ips/8.8.8.8 - response: - body: - string: !!binary | - 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 - yJJrB5TvL3/6y/8HdXNTjxy/CAA= - headers: - Access-Control-Allow-Origin: - - https://spycloud-external.readme.io - Connection: - - keep-alive - Content-Encoding: - - gzip - Content-Length: - - '103589' - Content-Type: - - application/json - Date: - - Fri, 11 Jul 2025 18:22:05 GMT - Via: - - 1.1 20e1ec5c4961778268603d507aa565a0.cloudfront.net (CloudFront) - X-Amz-Cf-Id: - - 9vJcGrmq2lgOxSAXTstWJGhnfPFqhl7mhH7k55kg4ExaiigLZhw3mQ== - X-Amz-Cf-Pop: - - ATL56-P2 - X-Amzn-Trace-Id: - - Root=1-6871564a-483337b646e7cfb9409e8cc7;Parent=558346f744e1c96b;Sampled=0;Lineage=2:09b13bf6:0 - X-Cache: - - Miss from cloudfront - x-amz-apigw-id: - - NjprvF9EoAMEoTA= - x-amzn-RequestId: - - 79eecd0c-ef10-4c22-9934-001ade2c8ca2 - status: - code: 200 - message: OK -version: 1 diff --git a/ppp_connectors/tests/test_spycloud/test_integration_spycloud.py b/ppp_connectors/tests/test_spycloud/test_integration_spycloud.py deleted file mode 100644 index 9b6085e..0000000 --- a/ppp_connectors/tests/test_spycloud/test_integration_spycloud.py +++ /dev/null @@ -1,12 +0,0 @@ -import httpx -import pytest -from ppp_connectors.api_connectors.spycloud import SpycloudConnector - -@pytest.mark.integration -def test_spycloud_ato_search_vcr(vcr_cassette): - with vcr_cassette.use_cassette("test_spycloud_ato_search_vcr"): - connector = SpycloudConnector(load_env_vars=True, enable_logging=True) - result = connector.ato_search(search_type="ip", query="8.8.8.8") - - assert isinstance(result, httpx.Response) - assert "results" in result.json() \ No newline at end of file diff --git a/ppp_connectors/tests/test_spycloud/test_unit_async_spycloud.py b/ppp_connectors/tests/test_spycloud/test_unit_async_spycloud.py deleted file mode 100644 index 53a9fc2..0000000 --- a/ppp_connectors/tests/test_spycloud/test_unit_async_spycloud.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest -import httpx -from ppp_connectors.api_connectors.spycloud import AsyncSpycloudConnector - -@pytest.mark.asyncio -async def test_async_init_with_env_keys(monkeypatch): - monkeypatch.setenv("SPYCLOUD_API_SIP_KEY", "sip") - monkeypatch.setenv("SPYCLOUD_API_ATO_KEY", "ato") - monkeypatch.setenv("SPYCLOUD_API_INV_KEY", "inv") - - conn = AsyncSpycloudConnector(load_env_vars=True) - assert conn.sip_key == "sip" - assert conn.ato_key == "ato" - assert conn.inv_key == "inv" - -@pytest.mark.asyncio -async def test_async_sip_cookie_domains_sends(mocker): - mock_response = mocker.AsyncMock(spec=httpx.Response) - request = mocker.patch("httpx.AsyncClient.request", return_value=mock_response) - - connector = AsyncSpycloudConnector(sip_key="abc123") - result = await connector.sip_cookie_domains("test.com", q="xyz") - - assert result is mock_response - request.assert_called_once() - args, kwargs = request.call_args - assert kwargs["url"].endswith("/sip-v1/breach/data/cookie-domains/test.com") - assert kwargs["headers"]["x-api-key"] == "abc123" - assert kwargs["params"]["q"] == "xyz" - -@pytest.mark.asyncio -async def test_async_ato_search_ip(mocker): - mock_response = mocker.AsyncMock(spec=httpx.Response) - mocker.patch("httpx.AsyncClient.request", return_value=mock_response) - - connector = AsyncSpycloudConnector(ato_key="abc123") - resp = await connector.ato_search("ip", "1.2.3.4") - assert resp is mock_response - -@pytest.mark.asyncio -async def test_async_investigations_invalid_type_raises(): - conn = AsyncSpycloudConnector(inv_key="abc") - with pytest.raises(ValueError): - await conn.investigations_search("not-a-real-type", "foo") \ No newline at end of file diff --git a/ppp_connectors/tests/test_spycloud/test_unit_spycloud.py b/ppp_connectors/tests/test_spycloud/test_unit_spycloud.py deleted file mode 100644 index 0924b71..0000000 --- a/ppp_connectors/tests/test_spycloud/test_unit_spycloud.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -import httpx -import pytest -from unittest.mock import patch, MagicMock -from ppp_connectors.api_connectors.spycloud import SpycloudConnector - - -def test_init_with_api_key(): - connector = SpycloudConnector(sip_key="sip", ato_key="ato", inv_key="inv") - assert connector.sip_key == "sip" - assert connector.ato_key == "ato" - assert connector.inv_key == "inv" - - -@patch.dict("os.environ", {"SPYCLOUD_API_ATO_KEY": "env_key"}, clear=True) -def test_init_with_env_key(): - connector = SpycloudConnector(load_env_vars=True) - assert connector.ato_key == "env_key" - - -@patch("ppp_connectors.api_connectors.broker.combine_env_configs", return_value={}) -def test_init_missing_key(mock_env): - connector = SpycloudConnector(load_env_vars=True) - with pytest.raises(ValueError, match="SPYCLOUD_API_ATO_KEY is required for this request"): - connector.ato_search(search_type="email", query="test@example.com") - - -@patch("ppp_connectors.api_connectors.spycloud.SpycloudConnector._make_request") -def test_ato_search(mock_request): - # Build a real httpx.Response to match new return type (raw Response) - request = httpx.Request("GET", "https://api.spycloud.io/sp-v2/breach/data/emails/test@example.com") - payload = {"results": []} - mock_response = httpx.Response( - 200, - request=request, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_request.return_value = mock_response - - connector = SpycloudConnector(ato_key="test_key") - result = connector.ato_search(search_type="ip", query="test@example.com") - - mock_request.assert_called_once() - assert isinstance(result, httpx.Response) - assert result.json() == payload \ No newline at end of file diff --git a/ppp_connectors/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml b/ppp_connectors/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml deleted file mode 100644 index d9d7a57..0000000 --- a/ppp_connectors/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml +++ /dev/null @@ -1,68 +0,0 @@ -interactions: -- request: - body: '' - headers: - Authorization: - - DUMMY - accept: - - '*/*' - accept-encoding: - - gzip, deflate - connection: - - keep-alive - host: - - lookups.twilio.com - user-agent: - - python-httpx/0.28.1 - method: GET - uri: https://lookups.twilio.com/v2/PhoneNumbers/+14155552671?Fields= - response: - body: - string: !!binary | - H4sIAAAAAAAAAG2Q3UrEMBCFX6X0SlE3dLEK+xCLIF6JhJjOtmHz58xkSxHf3aQorcXcDDln+M5h - PmutrJWngKPCzvi+PlQ+WXtbzQag9MrBXzFvSR2SZ5zy7IpbN3UxN+LLc1FNB54NT9Ip1sOCyhyQ - xIoTbUSeIkjjGXJWD16v8r1iE7yaG2deSbm6b9rrqm3bu/3D49wjDiFTfHLvgGXjpskr+f3ny4+k - bGlHOuAqKCLIk7F2URAUkek9dAv6xyLjJI0qrhRHMiYXy63Q0HlxEhZmPTBHOghhQzinSDsejTVh - p4MTl714Kg2PcwqJbf1LLtxlBmOC3+98FgmIAcs1X6u3r29cgXV62wEAAA== - headers: - Connection: - - keep-alive - Content-Encoding: - - gzip - Content-Length: - - '271' - Content-Type: - - application/json;charset=utf-8 - Date: - - Fri, 11 Jul 2025 17:18:13 GMT - Strict-Transport-Security: - - max-age=31536000 - Twilio-Concurrent-Requests: - - '1' - Twilio-Request-Duration: - - '0.072' - Twilio-Request-Id: - - RQc8a2e45336e41fa56ef57dc5d1b07623 - Vary: - - Accept-Encoding - - Origin - Via: - - 1.1 b8ac2f92eb514fa1ca7a47e834b5044e.cloudfront.net (CloudFront) - X-API-Domain: - - lookups.twilio.com - X-Amz-Cf-Id: - - -8mzkQVCupui1I0KUXPtHbjmOB3ubY0xB7iaWxCQpMjBxGQ-jAOikw== - X-Amz-Cf-Pop: - - ATL59-P9 - X-Cache: - - Miss from cloudfront - X-Home-Region: - - us1 - X-Powered-By: - - AT-5000 - X-Shenanigans: - - none - status: - code: 200 - message: OK -version: 1 diff --git a/ppp_connectors/tests/test_twilio/test_integration_twilio.py b/ppp_connectors/tests/test_twilio/test_integration_twilio.py deleted file mode 100644 index ad5c8ff..0000000 --- a/ppp_connectors/tests/test_twilio/test_integration_twilio.py +++ /dev/null @@ -1,14 +0,0 @@ - -import httpx -import pytest -import vcr -from ppp_connectors.api_connectors.twilio import TwilioConnector - -@pytest.mark.integration -def test_lookup_phone_vcr(vcr_cassette): - with vcr_cassette.use_cassette("test_lookup_phone_vcr"): - connector = TwilioConnector(load_env_vars=True) - result = connector.lookup_phone("+14155552671") - - assert isinstance(result, httpx.Response) - assert "phone_number" in result.json() or "caller_name" in result.json() or "line_type" in result.json() \ No newline at end of file diff --git a/ppp_connectors/tests/test_twilio/test_unit_async_twilio.py b/ppp_connectors/tests/test_twilio/test_unit_async_twilio.py deleted file mode 100644 index 4c1689d..0000000 --- a/ppp_connectors/tests/test_twilio/test_unit_async_twilio.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest -import httpx -from unittest.mock import patch, AsyncMock -from ppp_connectors.api_connectors.twilio import AsyncTwilioConnector - -@pytest.mark.asyncio -async def test_async_init_with_env(monkeypatch): - monkeypatch.setenv("TWILIO_API_SID", "sid") - monkeypatch.setenv("TWILIO_API_SECRET", "secret") - - connector = AsyncTwilioConnector(load_env_vars=True) - assert connector.api_sid == "sid" - assert connector.api_secret == "secret" - -@pytest.mark.asyncio -async def test_async_lookup_phone_raises_for_invalid_packages(): - connector = AsyncTwilioConnector(api_sid="a", api_secret="b") - with pytest.raises(ValueError, match="Invalid data packages: badpkg"): - await connector.lookup_phone("+14155552671", data_packages=["badpkg"]) - -@patch("ppp_connectors.api_connectors.twilio.AsyncTwilioConnector._make_request", new_callable=AsyncMock) -@pytest.mark.asyncio -async def test_async_lookup_phone_calls_make_request(mock_request): - mock_resp = httpx.Response(200, content=b'{"valid": true}') - mock_request.return_value = mock_resp - - connector = AsyncTwilioConnector(api_sid="a", api_secret="b") - result = await connector.lookup_phone("+14155552671", data_packages=["caller_name"]) - - assert result.status_code == 200 - assert mock_request.await_count == 1 - args, kwargs = mock_request.call_args - assert kwargs["endpoint"].endswith("+14155552671") - assert "caller_name" in kwargs["params"]["Fields"] \ No newline at end of file diff --git a/ppp_connectors/tests/test_twilio/test_unit_twilio.py b/ppp_connectors/tests/test_twilio/test_unit_twilio.py deleted file mode 100644 index 3d84c88..0000000 --- a/ppp_connectors/tests/test_twilio/test_unit_twilio.py +++ /dev/null @@ -1,45 +0,0 @@ -import httpx -import pytest -from unittest.mock import patch, MagicMock -from ppp_connectors.api_connectors.twilio import TwilioConnector - - -def test_init_with_all_keys(): - connector = TwilioConnector(api_sid="sid", api_secret="secret") - assert connector.api_sid == "sid" - assert connector.auth is not None - - -@patch.dict("os.environ", {"TWILIO_API_SID": "sid", "TWILIO_API_SECRET": "secret"}, clear=True) -def test_init_with_env_keys(): - connector = TwilioConnector(load_env_vars=True) - assert connector.api_sid == "sid" - assert connector.auth is not None - - -@patch("ppp_connectors.api_connectors.broker.combine_env_configs", return_value={}) -def test_init_missing_auth_keys(mock_env): - with pytest.raises(ValueError, match="TWILIO_API_SID and TWILIO_API_SECRET are required"): - TwilioConnector(load_env_vars=True) - - -@patch("ppp_connectors.api_connectors.twilio.TwilioConnector._make_request") -def test_lookup_phone(mock_request): - import json - # Return a real httpx.Response to reflect new return type - req = httpx.Request("GET", "https://lookups.twilio.com/v2/PhoneNumbers/15555555555") - payload = {"carrier": "example"} - resp = httpx.Response( - 200, - request=req, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_request.return_value = resp - - connector = TwilioConnector(api_sid="sid", api_secret="secret") - result = connector.lookup_phone("15555555555") - - assert isinstance(result, httpx.Response) - assert result.json() == payload - mock_request.assert_called_once() \ No newline at end of file diff --git a/ppp_connectors/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml b/ppp_connectors/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml deleted file mode 100644 index 8a8d716..0000000 --- a/ppp_connectors/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml +++ /dev/null @@ -1,279 +0,0 @@ -interactions: -- request: - body: '' - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - api-key: - - REDACTED - connection: - - keep-alive - host: - - urlscan.io - user-agent: - - REDACTED - method: GET - uri: https://urlscan.io/api/v1/result/01958568-c986-7001-816d-9e0ccd7c4c4a - response: - body: - string: !!binary | - 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 - headers: - Cache-Control: - - private, max-age=60 - Connection: - - keep-alive - Content-Encoding: - - gzip - Content-Security-Policy: - - 'default-src ''self'' data: urlscan.io sentry.urlscan.io; script-src ''self'' - data: developers.google.com www.google.com www.gstatic.com urlscan.io sentry.urlscan.io; - style-src ''self'' fonts.googleapis.com www.google.com cdnjs.cloudflare.com - urlscan.io; img-src * data:; font-src ''self'' fonts.gstatic.com cdnjs.cloudflare.com - urlscan.io; child-src ''self''; frame-src https://www.google.com/recaptcha/; - form-action ''self''; connect-src ''self'' sentry.urlscan.io urlscan.io; upgrade-insecure-requests; - frame-ancestors ''none'';' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 11 Jul 2025 18:21:17 GMT - ETag: - - W/"13300-LhkzgtpQ7gn0AN/U3Gp0k02sIZs" - Referrer-Policy: - - same-origin - Server: - - nginx - Strict-Transport-Security: - - max-age=63072000; includeSubdomains; preload - Transfer-Encoding: - - chunked - Vary: - - Accept-Encoding - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - X-Proxy-Cache: - - MISS - X-Rate-Limit-Action: - - retrieve - X-Rate-Limit-Limit: - - '2500' - X-Rate-Limit-Remaining: - - '2499' - X-Rate-Limit-Reset: - - '2025-07-11T18:22:00.000Z' - X-Rate-Limit-Reset-After: - - '42' - X-Rate-Limit-Scope: - - team - X-Rate-Limit-Window: - - minute - X-Robots-Tag: - - all - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/ppp_connectors/tests/test_urlscan/test_integration_urlscan.py b/ppp_connectors/tests/test_urlscan/test_integration_urlscan.py deleted file mode 100644 index 80c6a1d..0000000 --- a/ppp_connectors/tests/test_urlscan/test_integration_urlscan.py +++ /dev/null @@ -1,12 +0,0 @@ -import httpx -import pytest -from ppp_connectors.api_connectors.urlscan import URLScanConnector - -@pytest.mark.integration -def test_urlscan_results_vcr(vcr_cassette): - with vcr_cassette.use_cassette("test_urlscan_results_vcr"): - connector = URLScanConnector(load_env_vars=True, enable_logging=True) - result = connector.results("01958568-c986-7001-816d-9e0ccd7c4c4a") - - assert isinstance(result, httpx.Response) - assert "task" in result.json() \ No newline at end of file diff --git a/ppp_connectors/tests/test_urlscan/test_unit_async_urlscan.py b/ppp_connectors/tests/test_urlscan/test_unit_async_urlscan.py deleted file mode 100644 index f5f2250..0000000 --- a/ppp_connectors/tests/test_urlscan/test_unit_async_urlscan.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -from httpx import Response -from httpx import Request -from ppp_connectors.api_connectors.urlscan import AsyncURLScanConnector - -@pytest.mark.asyncio -async def test_async_init_with_api_key(): - connector = AsyncURLScanConnector(api_key="testkey") - assert connector.api_key == "testkey" - assert connector.headers["API-Key"] == "testkey" - -@pytest.mark.asyncio -async def test_async_init_with_env(monkeypatch): - monkeypatch.setenv("URLSCAN_API_KEY", "envkey") - connector = AsyncURLScanConnector(load_env_vars=True) - assert connector.api_key == "envkey" - assert connector.headers["API-Key"] == "envkey" - -@pytest.mark.asyncio -async def test_async_init_missing_key_raises(monkeypatch): - monkeypatch.delenv("URLSCAN_API_KEY", raising=False) - with pytest.raises(ValueError, match="API key is required for AsyncURLScanConnector"): - AsyncURLScanConnector() - -@pytest.mark.asyncio -async def test_async_search_makes_expected_call(monkeypatch): - connector = AsyncURLScanConnector(api_key="testkey") - - async def mock_get(path, params=None): - assert path == "/api/v1/search/" - assert params["q"] == "example.com" - return Response(200, request=Request("GET", path)) - - connector.get = mock_get - resp = await connector.search("example.com") - assert resp.status_code == 200 - -@pytest.mark.asyncio -async def test_async_scan_makes_expected_call(monkeypatch): - connector = AsyncURLScanConnector(api_key="testkey") - - async def mock_post(path, json=None): - assert path == "/api/v1/scan" - assert json["url"] == "http://example.com" - return Response(200, request=Request("POST", path)) - - connector.post = mock_post - resp = await connector.scan("http://example.com") - assert resp.status_code == 200 \ No newline at end of file diff --git a/ppp_connectors/tests/test_urlscan/test_unit_urlscan.py b/ppp_connectors/tests/test_urlscan/test_unit_urlscan.py deleted file mode 100644 index 345681b..0000000 --- a/ppp_connectors/tests/test_urlscan/test_unit_urlscan.py +++ /dev/null @@ -1,39 +0,0 @@ -import httpx -import pytest -from unittest.mock import patch, MagicMock -from ppp_connectors.api_connectors.urlscan import URLScanConnector - -def test_init_with_api_key(): - connector = URLScanConnector(api_key="test_key") - assert connector.api_key == "test_key" - -@patch.dict("os.environ", {"URLSCAN_API_KEY": "env_key"}, clear=True) -def test_init_with_env_key(): - connector = URLScanConnector(load_env_vars=True) - assert connector.api_key == "env_key" - -@patch("ppp_connectors.api_connectors.broker.combine_env_configs", return_value={}) -def test_init_missing_auth_keys(mock_env): - with pytest.raises(ValueError, match="API key is required for URLScanConnector"): - URLScanConnector(load_env_vars=True) - -@patch("ppp_connectors.api_connectors.urlscan.URLScanConnector.get") -def test_results(mock_get): - import json - - # Build a real httpx.Response to match the new return type - request = httpx.Request("GET", "https://urlscan.io/api/v1/result/abc123") - payload = {"task": "test"} - mock_response = httpx.Response( - 200, - request=request, - content=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - ) - mock_get.return_value = mock_response - - connector = URLScanConnector(api_key="test_key") - result = connector.results("abc123") - - assert isinstance(result, httpx.Response) - assert result.json() == payload \ No newline at end of file