-
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 abstraction for resources and subsequent implementation of users. #20
Conversation
0785264
to
344e905
Compare
☂️ Python Coverage
Overall Coverage
New Files
Modified Files
|
344e905
to
6a928cb
Compare
from posit.connect.client import create_client | ||
|
||
client = Client() | ||
res = client.users.get_current_user() | ||
print(res.json()) | ||
with create_client() as client: | ||
print(client.users.get("f55ca95d-ce52-43ed-b31b-48dc4a07fe13")) | ||
|
||
users = client.users | ||
users = users.find(lambda user: user["first_name"].startswith("T")) | ||
users = users.find(lambda user: user["last_name"].startswith("S")) | ||
user = users.find_one(lambda user: user["user_role"] == "administrator") | ||
print(user) |
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.
Here is some example usage.
from typing import Generic, Iterator, Optional, TypeVar, List, TypedDict, Tuple | ||
|
||
|
||
class Resource(TypedDict): |
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.
@nealrichardson - TypedDict
seems to be the best of both worlds. We get dict
behavior and typed autocomplete, linting, etc...
def find_one(self, filter: Callable[[User], bool] = lambda _: True) -> User | None: | ||
return next((user for user in self if filter(user)), None) |
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.
I'm currently in favor of using functions as arguments for filtering. This allows for more expressive filtering.
This also takes advantage of the lazy-iterable implementation. Once a match is found here, the iterable stops, meaning subsequent fetches to the server are skipped.
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.
I think this is nice, and I like the expressiveness, but it's not compatible with server-side querying/searching, and it also doesn't help the user discover the valid parameter names. It's also annoying to have to write a lambda when I want an exact match (e.g. user.email == me).
So my initial take is +1 for having a filter
argument like this, but it shouldn't be the only way.
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.
Ah, yes, thanks for the reminder regarding server-side params.
if (index % self.page_size) != 0: | ||
# | ||
raise ValueError( | ||
f"index ({index}) must be a multiple of page size ({self.page_size})" | ||
) |
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.
Additional logic can be implemented to handle partial fetches, but it's unnecessary since this should only be called at the start of a new page.
def get(self, id: str) -> User: | ||
user = next((user for user in self if user["guid"] == id), None) | ||
if user is None: | ||
raise RuntimeError(f"failed to get user with id '{id}'") | ||
return user |
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.
Notice that there are implementations of get
in Users
and LazyUsers
.
The following implementations therefore have slightly different behavior.
LazyUsers(...).get(...)
will call the server.
Users(...).get(...)
will NOT call the server.
LazyUsers(...).find(...).get(...)
will NOT call the server since .find
returns an instance of Users
, not LazyUsers
.
Generally, an in-memory list iteration will execute faster than a client/server round-trip.
There are other implications that need to be addressed. E.g., what happens when we implement .create
?
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.
How does one get LazyUsers or just Users?
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.
The current entry point is client.users
:
posit-sdk-py/src/posit/connect/client.py
Line 57 in 6a928cb
self.users: Users = LazyUsers(config=config, session=session) |
with create_client(...) as client:
users = client.users.find(...).get(...)
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.
Naming is tough, but if I'm inspecting types I get back from the client it might be nice to see something more like Users
and CachedUsers
.
- if: always() | ||
uses: orgoro/[email protected] | ||
with: | ||
coverageFile: coverage.xml | ||
token: ${{ secrets.GITHUB_TOKEN }} |
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.
cov will fails < 100%, but show the report anyways in the pull request.
I'm going to merge this to get things on |
from typing import Optional | ||
|
||
|
||
def _get_api_key() -> str: |
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.
You might consider using NewType to make distinct string types for things that have special meaning (API keys, server URLs, endpoints, ...).
endpoint = os.path.join(self._endpoint, "__api__/v1/user") | ||
return self._session.get(endpoint) | ||
def get(self, id: str) -> User: | ||
endpoint = os.path.join(self.config.endpoint, "v1/users", id) |
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.
I think you'll need urllib.parse.urljoin
or posixpath.join
, because os.path.join
will use os.sep
which will be a backslash on Windows.
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.
Thanks. I'll get a safer implementation together! This was quick and dirty :)
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.
import os | ||
import requests | ||
|
||
_MAX_PAGE_SIZE = 500 |
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.
it looks like we aren't using max_page_size as an enforcement tool, but rather as a default value in some places. Wondering if you intend to actually enforce a max page size anywhere - if not, maybe this variable should have a different name?
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.
Ah, yes. Good point. _MAX_PAGE_SIZE
means "this is the max page size I can use," but you're right. _PAGE_SIZE
plus some ValueError validation would be better.
See comments below.
I will come back around and implement the full unit test suite for
resources.py
andusers.py
.