Skip to content

Commit

Permalink
feat: adds passthrough methods for each HTTP request type
Browse files Browse the repository at this point in the history
  • Loading branch information
tdstein committed Feb 16, 2024
1 parent 9dfc965 commit b618061
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 72 deletions.
112 changes: 96 additions & 16 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

from contextlib import contextmanager
from requests import Session

from requests import Response, Session
from typing import Generator, Optional

from . import hooks
Expand All @@ -13,18 +14,26 @@

@contextmanager
def create_client(
api_key: Optional[str] = None, endpoint: Optional[str] = None
api_key: Optional[str] = None, url: Optional[str] = None
) -> Generator[Client, None, None]:
"""Creates a new :class:`Client` instance
"""
Creates a new :class:`Client` instance.
This function creates a new instance of the `Client` class, which is used to interact with the Posit Connect API.
Keyword Arguments:
api_key -- an api_key for authentication (default: {None})
endpoint -- a base api endpoint (url) (default: {None})
Args:
api_key (str, optional): An API key for authentication. Defaults to None.
url (str, optional): A base API URL. Defaults to None.
Returns:
A :class:`Client` instance
Yields:
Client: A `Client` instance.
Example:
>>> with create_client(api_key='my_api_key', url='https://api.example.com') as client:
... # Use the client to make API calls
... response = client.get('/users')
"""
client = Client(api_key=api_key, endpoint=endpoint)
client = Client(api_key=api_key, url=url)
try:
yield client
finally:
Expand All @@ -35,31 +44,102 @@ class Client:
def __init__(
self,
api_key: Optional[str] = None,
endpoint: Optional[str] = None,
url: Optional[str] = None,
) -> None:
"""
Initialize the Client instance.
Args:
api_key (str, optional): API key for authentication. Defaults to None.
endpoint (str, optional): API endpoint URL. Defaults to None.
url (str, optional): API url URL. Defaults to None.
"""
# Create a Config object.
config = Config(api_key=api_key, endpoint=endpoint)
self.config = Config(api_key=api_key, url=url)
# Create a Session object for making HTTP requests.
session = Session()
# Authenticate the session using the provided Config.
session.auth = Auth(config=config)
session.auth = Auth(config=self.config)
# Add error handling hooks to the session.
session.hooks["response"].append(hooks.handle_errors)

# Initialize the Users instance.
self.users: CachedUsers = Users(config=config, session=session)
self.users: CachedUsers = Users(config=self.config, session=session)
# Store the Session object.
self._session = session
self.session = session

def __del__(self):
"""
Close the session when the Client instance is deleted.
"""
self._session.close()
if hasattr(self, "session") and self.session is not None:
self.session.close()

def get(self, path: str, **kwargs) -> Response:
"""
Send a GET request to the specified path.
Args:
path (str): The path to send the request to.
**kwargs: Additional keyword arguments to be passed to the underlying session's `get` method.
Returns:
Response: The response object.
"""
return self.session.get(self.config.url.append(path), **kwargs)

def post(self, path: str, **kwargs) -> Response:
"""
Send a POST request to the specified path.
Args:
path (str): The path to send the request to.
**kwargs: Additional keyword arguments to be passed to the underlying session's `post` method.
Returns:
Response: The response object.
"""
return self.session.post(self.config.url.append(path), **kwargs)

def put(self, path: str, **kwargs) -> Response:
"""
Send a PUT request to the specified path.
Args:
path (str): The path to send the request to.
**kwargs: Additional keyword arguments to be passed to the underlying session's `put` method.
Returns:
Response: The response object.
"""
return self.session.put(self.config.url.append(path), **kwargs)

def patch(self, path: str, **kwargs) -> Response:
"""
Send a PATCH request to the specified path.
Args:
path (str): The path to send the request to.
**kwargs: Additional keyword arguments to be passed to the underlying session's `patch` method.
Returns:
Response: The response object.
"""
return self.session.patch(self.config.url.append(path), **kwargs)

def delete(self, path: str, **kwargs) -> Response:
"""
Send a DELETE request to the specified path.
Args:
path (str): The path to send the request to.
**kwargs: Additional keyword arguments to be passed to the underlying session's `delete` method.
Returns:
Response: The response object.
"""
return self.session.delete(self.config.url.append(path), **kwargs)
112 changes: 86 additions & 26 deletions src/posit/connect/client_test.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,103 @@
from unittest.mock import MagicMock, patch
import pytest

from .client import Client, create_client


@pytest.fixture
def MockAuth():
with patch("posit.connect.client.Auth") as mock:
yield mock


@pytest.fixture
def MockClient():
with patch("posit.connect.client.Client") as mock:
yield mock


@pytest.fixture
def MockConfig():
with patch("posit.connect.client.Config") as mock:
yield mock


@pytest.fixture
def MockSession():
with patch("posit.connect.client.Session") as mock:
yield mock


@pytest.fixture
def MockUsers():
with patch("posit.connect.client.Users") as mock:
yield mock


class TestCreateClient:
@patch("posit.connect.client.Client")
def test(self, Client: MagicMock):
def test(self, MockClient: MagicMock):
api_key = "foobar"
endpoint = "http://foo.bar"
with create_client(api_key=api_key, endpoint=endpoint) as client:
assert client == Client.return_value
url = "http://foo.bar"
with create_client(api_key=api_key, url=url) as client:
assert client == MockClient.return_value


class TestClient:
@patch("posit.connect.client.Users")
@patch("posit.connect.client.Session")
@patch("posit.connect.client.Config")
@patch("posit.connect.client.Auth")
def test_init(
self,
Auth: MagicMock,
Config: MagicMock,
Session: MagicMock,
Users: MagicMock,
MockAuth: MagicMock,
MockConfig: MagicMock,
MockSession: MagicMock,
MockUsers: MagicMock,
):
api_key = "foobar"
endpoint = "http://foo.bar"
Client(api_key=api_key, endpoint=endpoint)
config = Config.return_value
Auth.assert_called_once_with(config=config)
Config.assert_called_once_with(api_key=api_key, endpoint=endpoint)
Session.assert_called_once()
Users.assert_called_once_with(config=config, session=Session.return_value)
url = "http://foo.bar"
Client(api_key=api_key, url=url)
MockAuth.assert_called_once_with(config=MockConfig.return_value)
MockConfig.assert_called_once_with(api_key=api_key, url=url)
MockSession.assert_called_once()
MockUsers.assert_called_once_with(
config=MockConfig.return_value, session=MockSession.return_value
)

@patch("posit.connect.client.Session")
@patch("posit.connect.client.Auth")
def test_del(self, Auth: MagicMock, Session: MagicMock):
def test_del(self, MockAuth, MockSession):
api_key = "foobar"
endpoint = "http://foo.bar"
client = Client(api_key=api_key, endpoint=endpoint)
url = "http://foo.bar"
client = Client(api_key=api_key, url=url)
del client
Session.return_value.close.assert_called_once()
MockSession.return_value.close.assert_called_once()

def test_get(self, MockSession):
api_key = "foobar"
url = "http://foo.bar"
client = Client(api_key=api_key, url=url)
client.get("/foo")
client.session.get.assert_called_once_with("http://foo.bar/foo")

def test_post(self, MockSession):
api_key = "foobar"
url = "http://foo.bar"
client = Client(api_key=api_key, url=url)
client.post("/foo")
client.session.post.assert_called_once_with("http://foo.bar/foo")

def test_put(self, MockSession):
api_key = "foobar"
url = "http://foo.bar"
client = Client(api_key=api_key, url=url)
client.put("/foo")
client.session.put.assert_called_once_with("http://foo.bar/foo")

def test_patch(self, MockSession):
api_key = "foobar"
url = "http://foo.bar"
client = Client(api_key=api_key, url=url)
client.patch("/foo")
client.session.patch.assert_called_once_with("http://foo.bar/foo")

def test_delete(self, MockSession):
api_key = "foobar"
url = "http://foo.bar"
client = Client(api_key=api_key, url=url)
client.delete("/foo")
client.session.delete.assert_called_once_with("http://foo.bar/foo")
21 changes: 7 additions & 14 deletions src/posit/connect/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os

from typing import Optional

from .urls import Url


def _get_api_key() -> str:
"""Gets the API key from the environment variable 'CONNECT_API_KEY'.
Expand All @@ -13,14 +14,14 @@ def _get_api_key() -> str:
The API key
"""
value = os.environ.get("CONNECT_API_KEY")
if value is None or value == "":
if not value:
raise ValueError(
"Invalid value for 'CONNECT_API_KEY': Must be a non-empty string."
)
return value


def _get_endpoint() -> str:
def _get_url() -> str:
"""Gets the endpoint from the environment variable 'CONNECT_SERVER'.
The `requests` library uses 'endpoint' instead of 'server'. We will use 'endpoint' from here forward for consistency.
Expand All @@ -32,26 +33,18 @@ def _get_endpoint() -> str:
The endpoint.
"""
value = os.environ.get("CONNECT_SERVER")
if value is None or value == "":
if not value:
raise ValueError(
"Invalid value for 'CONNECT_SERVER': Must be a non-empty string."
)
return value


def _format_endpoint(endpoint: str) -> str:
# todo - format endpoint url and ake sure it ends with __api__
return endpoint


class Config:
"""Derived configuration properties"""

api_key: str
endpoint: str

def __init__(
self, api_key: Optional[str] = None, endpoint: Optional[str] = None
self, api_key: Optional[str] = None, url: Optional[str] = None
) -> None:
self.api_key = api_key or _get_api_key()
self.endpoint = _format_endpoint(endpoint or _get_endpoint())
self.url = Url(url or _get_url())
24 changes: 13 additions & 11 deletions src/posit/connect/config_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest

from unittest.mock import patch
from unittest.mock import MagicMock, patch

from .config import Config, _get_api_key, _get_endpoint
from .config import Config, _get_api_key, _get_url


class TestGetApiKey:
Expand All @@ -21,26 +21,28 @@ def test_get_api_key_miss(self):
_get_api_key()


class TestGetEndpoint:
class TestGetUrl:
@patch.dict("os.environ", {"CONNECT_SERVER": "http://foo.bar"})
def test_get_endpoint(self):
endpoint = _get_endpoint()
assert endpoint == "http://foo.bar"
url = _get_url()
assert url == "http://foo.bar"

@patch.dict("os.environ", {"CONNECT_SERVER": ""})
def test_get_endpoint_empty(self):
with pytest.raises(ValueError):
_get_endpoint()
_get_url()

def test_get_endpoint_miss(self):
with pytest.raises(ValueError):
_get_endpoint()
_get_url()


class TestConfig:
def test_init(self):
@patch("posit.connect.config.Url")
def test_init(self, Url: MagicMock):
api_key = "foobar"
endpoint = "http://foo.bar"
config = Config(api_key=api_key, endpoint=endpoint)
url = "http://foo.bar"
config = Config(api_key=api_key, url=url)
assert config.api_key == api_key
assert config.endpoint == endpoint
assert config.url == Url.return_value
Url.assert_called_with(url)
Loading

0 comments on commit b618061

Please sign in to comment.