-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds support for backwards and forward compatability
- Loading branch information
Showing
6 changed files
with
219 additions
and
39 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
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,49 @@ | ||
from abc import ABC, abstractmethod | ||
from dataclasses import asdict, dataclass, fields | ||
from typing import Generic, List, Optional, TypeVar | ||
|
||
|
||
T = TypeVar("T") | ||
|
||
|
||
@dataclass | ||
class Resource(ABC): | ||
def __init__(self, **kwargs) -> None: | ||
mapping = {self._compatibility.get(k, k): v for k, v in kwargs.items()} | ||
for prop in fields(self): | ||
setattr( | ||
self, prop.name, mapping.get(prop.name, kwargs.get(prop.name, None)) | ||
) | ||
|
||
@property | ||
def _compatibility(self): | ||
return {} | ||
|
||
def asdict(self) -> dict: | ||
return asdict(self) | ||
|
||
|
||
class Resources(ABC, Generic[T]): | ||
@abstractmethod | ||
def create(self, *args, **kwargs) -> T: | ||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def delete(self, *args, **kwargs) -> None: | ||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def find(self, *args, **kwargs) -> List[T]: | ||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def find_one(self, *args, **kwargs) -> Optional[T]: | ||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def get(self, *args, **kwargs) -> T: | ||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def update(self, *args, **kwargs) -> T: | ||
raise NotImplementedError() |
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,115 @@ | ||
import pytest | ||
|
||
from dataclasses import dataclass | ||
from typing import Any, List, Optional | ||
|
||
from posit.connect.resources import Resources, Resource | ||
|
||
|
||
@dataclass(init=False) | ||
class FakeResource(Resource): | ||
foo: str | ||
bar: str | ||
|
||
@property | ||
def _compatibility(self): | ||
return {"backwards": "foo"} | ||
|
||
@property | ||
def backwards(self): | ||
"""A field which was removed from the API. | ||
This property provides backwards compatibility when a Connect is upgraded to a newer version which no longer provides "backwards". | ||
""" | ||
return self.foo | ||
|
||
|
||
class TestResource: | ||
def test_init(self): | ||
r = FakeResource(foo="foo", bar="bar") | ||
assert r.foo == "foo" | ||
assert r.bar == "bar" | ||
|
||
def test_from_dict(self): | ||
o = {"foo": "foo", "bar": "bar"} | ||
r = FakeResource(**o) | ||
assert r.foo == "foo" | ||
assert r.bar == "bar" | ||
|
||
def test_from_dict_with_missing_fields(self): | ||
o = {"bar": "bar"} | ||
r = FakeResource(**o) | ||
assert r.foo is None | ||
assert r.bar == "bar" | ||
|
||
def test_from_dict_with_additional_fields(self): | ||
o = {"foo": "foo", "bar": "bar", "baz": "baz"} | ||
r = FakeResource(**o) | ||
assert r.foo == "foo" | ||
|
||
def test_forwards_compatibility(self): | ||
o = {"foo": "foo", "bar": "bar"} | ||
r = FakeResource(**o) | ||
assert r.backwards == "foo" | ||
assert r.bar == "bar" | ||
|
||
def test_client_backwards_compatibility(self): | ||
o = {"foo": "foo", "bar": "bar"} | ||
r = FakeResource(**o) | ||
assert r.foo == "foo" | ||
assert r.bar == "bar" | ||
assert r.backwards == "foo" | ||
|
||
def test_server_backwards_compatibility(self): | ||
"""Asserts backwards compatibility for changes on the server. | ||
This checks that client code written for a current version of Connect can continue to function with previous versions of Connect when we implement backwards compatibility. | ||
""" | ||
o = {"bar": "bar", "backwards": "backwards"} | ||
r = FakeResource(**o) | ||
assert r.foo == "backwards" | ||
assert r.backwards == "backwards" | ||
|
||
|
||
class TestResources(Resources[Any]): | ||
def create(self) -> Any: | ||
return super().create() # type: ignore [safe-super] | ||
|
||
def delete(self) -> None: | ||
return super().delete() # type: ignore [safe-super] | ||
|
||
def find(self) -> List[Any]: | ||
return super().find() # type: ignore [safe-super] | ||
|
||
def find_one(self) -> Optional[Any]: | ||
return super().find_one() # type: ignore [safe-super] | ||
|
||
def get(self) -> Any: | ||
return super().get() # type: ignore [safe-super] | ||
|
||
def update(self) -> Any: | ||
return super().update() # type: ignore [safe-super] | ||
|
||
def test_create(self): | ||
with pytest.raises(NotImplementedError): | ||
self.create() | ||
|
||
def test_delete(self): | ||
with pytest.raises(NotImplementedError): | ||
self.delete() | ||
|
||
def test_find(self): | ||
with pytest.raises(NotImplementedError): | ||
self.find() | ||
|
||
def test_find_one(self): | ||
with pytest.raises(NotImplementedError): | ||
self.find_one() | ||
|
||
def test_get(self): | ||
with pytest.raises(NotImplementedError): | ||
self.get() | ||
|
||
def test_update(self): | ||
with pytest.raises(NotImplementedError): | ||
self.update() |
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 |
---|---|---|
@@ -1,14 +1,9 @@ | ||
import pandas | ||
|
||
from posit.connect import Client | ||
|
||
with Client() as client: | ||
user = client.users.get("f55ca95d-ce52-43ed-b31b-48dc4a07fe13") | ||
print(user.locked) | ||
print(user.is_locked) | ||
|
||
users = client.users.find() | ||
|
||
# This method of conversion provides forward compatibility against the API as fields are removed. This would require | ||
import pandas | ||
print(pandas.DataFrame(client.users.find())) | ||
print(pandas.DataFrame(client.content.find())) | ||
|
||
users = (user.asdict() for user in users) | ||
print(pandas.DataFrame(users)) | ||
print(client.me.asdict()) |