-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of HTTP connector
- Loading branch information
1 parent
b5baaa0
commit aa916be
Showing
12 changed files
with
400 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
"""HTTP-based tap class for Singer SDK.""" | ||
|
||
from __future__ import annotations | ||
|
||
import typing as t | ||
|
||
import requests | ||
|
||
from singer_sdk.authenticators import NoopAuth | ||
from singer_sdk.connectors.base import BaseConnector | ||
|
||
if t.TYPE_CHECKING: | ||
import sys | ||
|
||
from requests.adapters import BaseAdapter | ||
|
||
if sys.version_info >= (3, 10): | ||
from typing import TypeAlias # noqa: ICN003 | ||
else: | ||
from typing_extensions import TypeAlias | ||
|
||
_Auth: TypeAlias = t.Callable[[requests.PreparedRequest], requests.PreparedRequest] | ||
|
||
|
||
class HTTPConnector(BaseConnector[requests.Session]): | ||
"""Base class for all HTTP-based connectors.""" | ||
|
||
def __init__(self, config: t.Mapping[str, t.Any] | None) -> None: | ||
"""Initialize the HTTP connector. | ||
Args: | ||
config: Connector configuration parameters. | ||
""" | ||
super().__init__(config) | ||
self._session = self.get_session() | ||
self.refresh_auth() | ||
|
||
def get_connection(self, *, authenticate: bool = True) -> requests.Session: | ||
"""Return a new HTTP session object. | ||
Adds adapters and optionally authenticates the session. | ||
Args: | ||
authenticate: Whether to authenticate the request. | ||
Returns: | ||
A new HTTP session object. | ||
""" | ||
for prefix, adapter in self.adapters.items(): | ||
self._session.mount(prefix, adapter) | ||
|
||
self._session.auth = self._auth if authenticate else None | ||
|
||
return self._session | ||
|
||
def get_session(self) -> requests.Session: | ||
"""Return a new HTTP session object. | ||
Returns: | ||
A new HTTP session object. | ||
""" | ||
return requests.Session() | ||
|
||
def get_authenticator(self) -> _Auth: | ||
"""Authenticate the HTTP session. | ||
Returns: | ||
An auth callable. | ||
""" | ||
return NoopAuth() | ||
|
||
def refresh_auth(self) -> None: | ||
"""Refresh the HTTP session authentication.""" | ||
self._auth = self.get_authenticator() | ||
|
||
@property | ||
def adapters(self) -> dict[str, BaseAdapter]: | ||
"""Return a mapping of URL prefixes to adapter objects. | ||
Returns: | ||
A mapping of URL prefixes to adapter objects. | ||
""" | ||
return {} | ||
|
||
@property | ||
def default_request_kwargs(self) -> dict[str, t.Any]: | ||
"""Return default kwargs for HTTP requests. | ||
Returns: | ||
A mapping of default kwargs for HTTP requests. | ||
""" | ||
return {} | ||
|
||
def request( | ||
self, | ||
*args: t.Any, | ||
authenticate: bool = True, | ||
**kwargs: t.Any, | ||
) -> requests.Response: | ||
"""Make an HTTP request. | ||
Args: | ||
*args: Positional arguments to pass to the request method. | ||
authenticate: Whether to authenticate the request. | ||
**kwargs: Keyword arguments to pass to the request method. | ||
Returns: | ||
The HTTP response object. | ||
""" | ||
with self._connect(authenticate=authenticate) as session: | ||
kwargs = {**self.default_request_kwargs, **kwargs} | ||
return session.request(*args, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
"""Base class for all connectors.""" | ||
|
||
from __future__ import annotations | ||
|
||
import abc | ||
import typing as t | ||
from contextlib import contextmanager | ||
|
||
from singer_sdk.helpers._compat import Protocol | ||
|
||
_T = t.TypeVar("_T", covariant=True) | ||
|
||
|
||
class ContextManagerProtocol(Protocol[_T]): | ||
"""Protocol for context manager enter/exit.""" | ||
|
||
def __enter__(self) -> _T: # noqa: D105 | ||
... # pragma: no cover | ||
|
||
def __exit__(self, *args: t.Any) -> None: # noqa: D105 | ||
... # pragma: no cover | ||
|
||
|
||
_C = t.TypeVar("_C", bound=ContextManagerProtocol) | ||
|
||
|
||
class BaseConnector(abc.ABC, t.Generic[_C]): | ||
"""Base class for all connectors.""" | ||
|
||
def __init__(self, config: t.Mapping[str, t.Any] | None) -> None: | ||
"""Initialize the connector. | ||
Args: | ||
config: Plugin configuration parameters. | ||
""" | ||
self._config = config or {} | ||
|
||
@property | ||
def config(self) -> t.Mapping: | ||
"""Return the connector configuration. | ||
Returns: | ||
A mapping of configuration parameters. | ||
""" | ||
return self._config | ||
|
||
@contextmanager | ||
def _connect(self, *args: t.Any, **kwargs: t.Any) -> t.Generator[_C, None, None]: | ||
"""Connect to the destination. | ||
Args: | ||
args: Positional arguments to pass to the connection method. | ||
kwargs: Keyword arguments to pass to the connection method. | ||
Yields: | ||
A connection object. | ||
""" | ||
with self.get_connection(*args, **kwargs) as connection: | ||
yield connection | ||
|
||
@abc.abstractmethod | ||
def get_connection(self, *args: t.Any, **kwargs: t.Any) -> _C: | ||
"""Connect to the destination. | ||
Args: | ||
args: Positional arguments to pass to the connection method. | ||
kwargs: Keyword arguments to pass to the connection method. | ||
""" | ||
... |
Oops, something went wrong.