Skip to content

Commit

Permalink
implements the remainder of the MutableMapping interface
Browse files Browse the repository at this point in the history
  • Loading branch information
tdstein committed Jul 24, 2024
1 parent 9b874c3 commit 4964dc2
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 61 deletions.
106 changes: 47 additions & 59 deletions src/posit/connect/env.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
from __future__ import annotations

from typing import (
Any,
Dict,
List,
Iterable,
MutableMapping,
Optional,
overload,
)
from typing import Any, Iterator, List, Mapping, MutableMapping, Optional

from requests import Session

Expand All @@ -17,48 +9,59 @@
from .resources import Resources


class EnvVars(Resources):
class EnvVars(Resources, MutableMapping[str, Optional[str]]):
def __init__(
self, config: Config, session: Session, content_guid: str
) -> None:
super().__init__(config, session)
self.content_guid = content_guid

def __setitem__(self, key: str, value: str, /) -> None:
"""Set environment variable.
Set the environment variable for content.
def __delitem__(self, key: str, /) -> None:
"""Delete the environment variable.
Parameters
----------
key : str
The name of the environment variable to set.
value : str
The value assigned to the environment variable.
The name of the environment variable to delete.
Examples
--------
>>> vars = EnvVars(config, session, content_guid)
>>> vars["DATABASE_URL"] = (
... "postgres://user:password@localhost:5432/database"
... )
>>> del vars["DATABASE_URL"]
"""
self.update({key: value})
self.update({key: None})

def __delitem__(self, key: str, /) -> None:
"""Delete the environment variable.
def __getitem__(self, key: Any) -> Any:
raise NotImplementedError(
"Since environment variables may contain sensitive information, the values are not accessible outside of Connect."
)

def __iter__(self) -> Iterator:
return iter(self.find())

def __len__(self):
return len(self.find())

def __setitem__(self, key: str, value: Optional[str], /) -> None:
"""Set environment variable.
Set the environment variable for content.
Parameters
----------
key : str
The name of the environment variable to delete.
The name of the environment variable to set.
value : str
The value assigned to the environment variable.
Examples
--------
>>> vars = EnvVars(config, session, content_guid)
>>> del vars["DATABASE_URL"]
>>> vars["DATABASE_URL"] = (
... "postgres://user:password@localhost:5432/database"
... )
"""
self.update({key: None})
self.update({key: value})

def clear(self) -> None:
"""Remove all environment variables.
Expand Down Expand Up @@ -130,20 +133,12 @@ def find(self) -> List[str]:
response = self.session.get(url)
return response.json()

@overload
def update(
self, other: MutableMapping[str, Optional[str]], /, **kwargs: str
) -> None: ...

@overload
def update(
self, other: Iterable[tuple[str, Optional[str]]], /, **kwargs: str
) -> None: ...

@overload
def update(self, /, **kwargs: str) -> None: ...
def items(self):
raise NotImplementedError(
"Since environment variables may contain sensitive information, the values are not accessible outside of Connect."
)

def update(self, other=None, /, **kwargs: str) -> None:
def update(self, other=(), /, **kwargs: Optional[str]):
"""
Update environment variables.
Expand Down Expand Up @@ -193,26 +188,19 @@ def update(self, other=None, /, **kwargs: str) -> None:
... ]
... )
"""
d: Dict[str, str] = {}
if other is not None:
if isinstance(other, MutableMapping):
d.update(other)
elif isinstance(other, Iterable) and not isinstance(
other, (str, bytes)
):
try:
d.update(other)
except (TypeError, ValueError):
raise TypeError(
f"update expected a {MutableMapping} or {Iterable}, got {type(other)}"
)
else:
raise TypeError(
f"update expected a {MutableMapping} or {Iterable}, got {type(other)}"
)

if kwargs:
d.update(kwargs)
d = dict()
if isinstance(other, Mapping):
for key in other:
d[key] = other[key]
elif hasattr(other, "keys"):
for key in other.keys():
d[key] = other[key]
else:
for key, value in other:
d[key] = value

for key, value in kwargs.items():
d[key] = value

body = [{"name": key, "value": value} for key, value in d.items()]
path = f"v1/content/{self.content_guid}/environment"
Expand Down
169 changes: 167 additions & 2 deletions tests/posit/connect/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,102 @@ def test__delitem__():
assert mock_patch.call_count == 1


@responses.activate
def test__getitem__():
# data
guid = "f2f37341-e21d-3d80-c698-a935ad614066"

# behavior
responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}",
json=load_mock(f"v1/content/{guid}.json"),
)

responses.patch(
f"https://connect.example.com/__api__/v1/content/{guid}/environment",
json=[],
match=[
matchers.json_params_matcher(
[
{
"name": "TEST",
"value": None,
}
]
)
],
)

# setup
c = Client("https://connect.example.com", "12345")
content = c.content.get(guid)

# invoke
with pytest.raises(NotImplementedError):
content.environment_variables["TEST"]


@responses.activate
def test__iter__():
# data
guid = "f2f37341-e21d-3d80-c698-a935ad614066"

# behavior
mock_get_content = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}",
json=load_mock(f"v1/content/{guid}.json"),
)

mock_get_environment = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}/environment",
json=["TEST"],
)

# setup
c = Client("https://connect.example.com", "12345")
content = c.content.get(guid)

# invoke
iterator = iter(content.environment_variables)

# assert
assert next(iterator) == "TEST"
with pytest.raises(StopIteration):
next(iterator)

assert mock_get_content.call_count == 1
assert mock_get_environment.call_count == 1


@responses.activate
def test__len__():
# data
guid = "f2f37341-e21d-3d80-c698-a935ad614066"

# behavior
mock_get_content = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}",
json=load_mock(f"v1/content/{guid}.json"),
)

mock_get_environment = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}/environment",
json=["TEST"],
)

# setup
c = Client("https://connect.example.com", "12345")
content = c.content.get(guid)

# invoke
length = len(content.environment_variables)

# assert
assert length == 1
assert mock_get_content.call_count == 1
assert mock_get_environment.call_count == 1


@responses.activate
def test__setitem__():
# data
Expand Down Expand Up @@ -218,6 +314,31 @@ def test_find():
assert mock_get_environment.call_count == 1


@responses.activate
def test_items():
# data
guid = "f2f37341-e21d-3d80-c698-a935ad614066"

# behavior
mock_get_content = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}",
json=load_mock(f"v1/content/{guid}.json"),
)

mock_get_environment = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}/environment",
json=["TEST"],
)

# setup
c = Client("https://connect.example.com", "12345")
content = c.content.get(guid)

# invoke
with pytest.raises(NotImplementedError):
content.environment_variables.items()


class TestUpdate:
@responses.activate
def test(self):
Expand Down Expand Up @@ -257,7 +378,7 @@ def test(self):
assert mock_patch.call_count == 1

@responses.activate
def test_other_is_mutable_mapping(self):
def test_other_is_mapping(self):
# data
guid = "f2f37341-e21d-3d80-c698-a935ad614066"

Expand Down Expand Up @@ -293,6 +414,50 @@ def test_other_is_mutable_mapping(self):
assert mock_get.call_count == 1
assert mock_patch.call_count == 1

@responses.activate
def test_other_hasattr_keys(self):
# data
guid = "f2f37341-e21d-3d80-c698-a935ad614066"

# behavior
mock_get = responses.get(
f"https://connect.example.com/__api__/v1/content/{guid}",
json=load_mock(f"v1/content/{guid}.json"),
)

mock_patch = responses.patch(
f"https://connect.example.com/__api__/v1/content/{guid}/environment",
json=load_mock(f"v1/content/{guid}.json"),
match=[
matchers.json_params_matcher(
[
{
"name": "TEST",
"value": "TEST",
}
]
)
],
)

# setup
c = Client("https://connect.example.com", "12345")
content = c.content.get(guid)

class Test:
def __getitem__(self, key):
return "TEST"

def keys(self):
return ["TEST"]

# invoke
content.environment_variables.update(Test())

# assert
assert mock_get.call_count == 1
assert mock_patch.call_count == 1

@responses.activate
def test_other_is_iterable(self):
# data
Expand Down Expand Up @@ -380,7 +545,7 @@ def test_other_is_str(self):
content = c.content.get(guid)

# invoke
with pytest.raises(TypeError):
with pytest.raises(ValueError):
content.environment_variables.update("TEST")

@responses.activate
Expand Down

0 comments on commit 4964dc2

Please sign in to comment.