-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: adds base class definition for resources #83
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from abc import ABC, abstractmethod | ||
from typing import Generic, List, Optional, TypeVar | ||
|
||
|
||
T = TypeVar("T") | ||
|
||
|
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe? I assume that certain implementations may need to use a composite key for item references. |
||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def update(self, *args, **kwargs) -> T: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question as delete: would this be for bulk updates (regardless of whether the APIs would force us to do one request per entity)? |
||
raise NotImplementedError() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,10 +5,12 @@ | |
|
||
from requests import Session | ||
|
||
|
||
from . import urls | ||
|
||
from .config import Config | ||
from .paginator import _MAX_PAGE_SIZE, Paginator | ||
from .resources import Resources | ||
|
||
|
||
class User(TypedDict, total=False): | ||
|
@@ -25,7 +27,7 @@ class User(TypedDict, total=False): | |
locked: bool | ||
|
||
|
||
class Users: | ||
class Users(Resources[User]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What use is it to have More meta question: supposing you could factor some of those things out of the method definitions, should you? I could see an advantage of the current version being that it is explicit (famously better than implicit in Python ;), that you can read the class definition and see what it's doing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow. There may be something I need to learn about generics in Python. Using a parametrized type in the ABC enforces that the inheriting class has consistent return types across methods. Are you suggesting that there are scenarios where this won't be the case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess one place I'm confused is that we're still defining the return types in this subclass (e.g. L38, find() returns My other question is: for example, on L40, we call The other way I've seen this done is that you would set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case, consider I believe
They can, but when writing the implementing code, the Python language server will auto-complete with the correct type definitions. |
||
def __init__(self, config: Config, session: Session) -> None: | ||
self.url = urls.append_path(config.url, "v1/users") | ||
self.config = config | ||
|
@@ -54,3 +56,12 @@ def get(self, id: str) -> User: | |
url = urls.append_path(self.url, id) | ||
response = self.session.get(url) | ||
return User(**response.json()) | ||
|
||
def create(self) -> User: | ||
raise NotImplementedError() | ||
|
||
def update(self) -> User: | ||
raise NotImplementedError() | ||
|
||
def delete(self) -> None: | ||
raise NotImplementedError() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import pytest | ||
|
||
from typing import Any, List, Optional | ||
|
||
from posit.connect.resources import Resources | ||
|
||
|
||
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does
delete()
make sense for these collections? Would this be a bulk delete action?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on this method definition, the implementation can do either. We haven't tried injecting the session object into the associated Resource yet. This should be solved as part of #23.
Typically, I would expect this method to act on a collection, which would either be the syntax sugar of
user.delete() for user in users
(pseudo-code) or a direct call to a DELETE endpoint that supports deleting multiple items in a collection.