diff --git a/src/posit/connect/env.py b/src/posit/connect/env.py index 41ce21f8..bd6ab65a 100644 --- a/src/posit/connect/env.py +++ b/src/posit/connect/env.py @@ -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 @@ -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. @@ -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. @@ -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" diff --git a/tests/posit/connect/test_env.py b/tests/posit/connect/test_env.py index fb4a183c..a7fc0731 100644 --- a/tests/posit/connect/test_env.py +++ b/tests/posit/connect/test_env.py @@ -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 @@ -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): @@ -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" @@ -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 @@ -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