Skip to content
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 get_user and get_current_user methods. #7

Merged
merged 1 commit into from
Jan 31, 2024
Merged

Conversation

tdstein
Copy link
Collaborator

@tdstein tdstein commented Jan 25, 2024

Adds a new property to the Client class named users, which is an instance of a User class. The User class manages requests to the /v1/user and /v1/users endpoints.

Additionally, a hook is added to check and parse client errors from Connect.

Example

from posit.client import Client

client = Client()
res = client.users.get_current_user()
print(res.json())

Adds a new property to the Client class called 'users', which is an
instance of a User class. The User class is responsible for managing
requests to the /v1/user and /v1/users endpoints.

Additionally, a hook is added to check and parse client errors from
Connect.
Comment on lines +1 to +5
from posit.client import Client

client = Client()
res = client.users.get_current_user()
print(res.json())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the example shown in the Pull Request overview. You can invoke it as CONNECT_API_KEY=... CONNECT_SERVER=... python tinkering.py

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good (soon, not here) to start thinking about things like quartodoc or other ways to structure code examples that are runable... and what example server we might want to run against for that, if at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I like this idea a lot. I've used lots of Sphinx. The quartodoc setup looks similar.

I also had the thought to create a separate cookbooks module that's part of an extras package.

Comment on lines +20 to +31
if api_key:
builder.set_api_key(api_key)
if endpoint:
builder.set_endpoint(endpoint)
self._config = builder.build()

if self._config.api_key is None:
raise ValueError("Invalid value for 'api_key': Must be a non-empty string.")
if self._config.endpoint is None:
raise ValueError(
"Invalid value for 'endpoint': Must be a non-empty string."
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm making a strategic decision here to perform validation in the thing that needs the property. In this case, the Client needs the api_key and the endpoint.

I could argue that this should go in the ConfigBuilder#build method, but since I expect additional methods to consume Config, it makes sense to me to lay SoC here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I see the value of this ConfigBuilder factory abstraction. If Config needs to exist apart from Client, I'd more expect that the __init__ method for Config (and any subclasses thereof) would take the arguments it needs. Not that I'm the arbiter of it, but that feels more Pythonic to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tl;dr I'm planning a change to simplify this.

The crux of the idea is to provide some flexibility in how configuration attributes can be set. For example, you may want to set the endpoint URL inline or via CONNECT_SERVER. There may be other locations where configuration may come from, such as environment files, secret managers, and distributed stores, to name a few. The ConfigProvider definition provides this functionality.

The ConfigBuilder#build iterates through the providers in some priority order to decide on the in-memory value. Right now the priority is "in-line > environment property"

But who am I kidding? I over-engineered the mess out of that file, and I had fun doing it...

I'll see if simplifying it reduces the validation burden.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW what I have in mind is something along the lines of https://github.com/databricks/databricks-sdk-py/blob/main/databricks/sdk/__init__.py#L89-L143, though with fewer arguments. Basically:

class Client(object):
    def __init__(self, server=os.getenv("CONNECT_SERVER"), token=os.getenv("CONNECT_API_KEY"), **kwargs, config=None):
        if config is None:
            config = Config(server, token, **kwargs)
...

(could even omit kwargs for now since I don't think there's any other way today)

That way there could be future subclasses of Config, and those could be either provided explicitly as config, or we could wire up more logic in the Config init or whatever classmethod to detect.

Copy link
Collaborator

@nealrichardson nealrichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing forward. Some comments, more philosophical than the average PR review, but that seems fitting given where we are today 😂 Happy to chat live if that would help too.

Comment on lines +20 to +31
if api_key:
builder.set_api_key(api_key)
if endpoint:
builder.set_endpoint(endpoint)
self._config = builder.build()

if self._config.api_key is None:
raise ValueError("Invalid value for 'api_key': Must be a non-empty string.")
if self._config.endpoint is None:
raise ValueError(
"Invalid value for 'endpoint': Must be a non-empty string."
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I see the value of this ConfigBuilder factory abstraction. If Config needs to exist apart from Client, I'd more expect that the __init__ method for Config (and any subclasses thereof) would take the arguments it needs. Not that I'm the arbiter of it, but that feels more Pythonic to me.

self._endpoint = endpoint
self._session = session

def get_user(self, user_id: str) -> Response:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't had the design discussion on this, so take this is my opinion/taste on the matter, not strictly PR feedback. IMO it would be nice to have a common pattern to our collections of entities, like instead of client.users.get_user(user_id) client.content.get_content(content_id), more like:

client.users.find_one(id, name, email)
client.users.find(**kwargs)
client.content.find_one(id, title)
client.content.find_one(id).permissions.find(**kwargs)

etc.

Not attached to a particular verb per se, just that each collection has the same form, and the parameter names align. WTYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this model as well. It's been a while, but I recall using the ActiveRecord ORM pattern in Ruby-on-Rails. But, the advantage there was mapping relationships down to the SQL query layer for query optimization. We won't be able to do that here.

Do you have any prior art I could take a look at?

Let's see if we can get some consensus on a design pattern here. I'm too close to the implementation details on this one to know what our users would most enjoy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this isn't quite a SQL ORM. The model that came to mind first for me was pymongo, because we essentially have collections of records that we want to be able to (a) get one from or (b) get something iterable to scan over.

Other models we could look at could be pygithub, boto, [...] not an exhaustive list, just trying to think of other projects that users of this SDK may be familiar with and that have a similar objective--allow users to explore and manage collections of entities.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9


def get_user(self, user_id: str) -> Response:
endpoint = os.path.join(self._endpoint, "v1/users", user_id)
return self._session.get(endpoint)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what does this return exactly? Looks like requests.Response? We probably want something better than that. (Can make a separate issue.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Yes, we'll want something else. The most straightforward idea is to return the response body to the user on request. But I've been thinking through other ideas, like...

  • Return something that implements a to_dateframe (pandas) method.
  • Convert all responses into standard HATEOAS style that a paginator implementation understands.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#10

Comment on lines +1 to +5
from posit.client import Client

client = Client()
res = client.users.get_current_user()
print(res.json())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good (soon, not here) to start thinking about things like quartodoc or other ways to structure code examples that are runable... and what example server we might want to run against for that, if at all.

@tdstein tdstein merged commit 07cc7bf into main Jan 31, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants