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 partial users implementation based on lists #14

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/posit/connect/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
from .client import Client # noqa
from typing import Optional

from .client import Client


def make_client(
api_key: Optional[str] = None, endpoint: Optional[str] = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

what's this for? how is make_client() different from Client()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. Methods are slightly more "pythonic"/"idiomatic." And this is what boto3 does, so hopefully, it is more familiar to the average user.

Copy link
Collaborator

@nealrichardson nealrichardson Feb 11, 2024

Choose a reason for hiding this comment

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

I'm not seeing this.

What libraries are you thinking of where this is common?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

boto3.client('s3') and boto3.resource('s3') is what I'm referring to. https://github.com/boto/boto3/blob/develop/boto3/__init__.py#L86-L101

Both are factory methods which return a new instance based on the provided service_name.

) -> Client:
client = Client(api_key=api_key, endpoint=endpoint)
return client
76 changes: 67 additions & 9 deletions src/posit/connect/users.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,75 @@
from __future__ import annotations

import os

from requests import Session, Response
from dataclasses import dataclass, asdict
from datetime import datetime
from requests import Session
from typing import Optional


@dataclass
class User:
guid: str
email: str
username: str
first_name: str
last_name: str
user_role: str
created_time: datetime
updated_time: datetime
active_time: datetime
confirmed: bool
locked: bool

def to_dict(self) -> dict:
return asdict(self)


class Users(list[User]):
"""An extension of :class:`list[User]` with additional fetch methods."""

_endpoint: str
_session: Session

class Users:
def __init__(self, endpoint: str, session: Session) -> None:
def __init__(self, endpoint: str, session: Session):
self._endpoint = endpoint
self._session = session

def get_user(self, user_id: str) -> Response:
endpoint = os.path.join(self._endpoint, "__api__/v1/users", user_id)
return self._session.get(endpoint)
def find(self, params: dict = {}) -> Users:
"""Finds any :class:`User` that matches the provided filter conditions

Keyword Arguments:
params -- filter conditions (default: {{}})

Returns:
`self`
"""
self.clear()
endpoint = os.path.join(self._endpoint, "__api__/v1/users")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see this pattern in a few places, might want to make some URL constructor method for Client or something, which handles the __api__ prepending etc.

response = self._session.get(endpoint)
data = response.json()
for user in data["results"]:
if all(user.get(k) == v for k, v in params.items()):
self.append(User(**user))
# todo - implement paging and caching
return self

def find_one(self, params: dict = {}) -> Optional[User]:
"""Finds one :class:`User`

Keyword Arguments:
params -- filter conditions (default: {{}})

Returns:
A matching :class:`User`.
"""
if "guid" in params:
# Use the user details API if a 'guid' is provided.
# This is an example of how we can use different API endpoints to optimize execution time.
endpoint = os.path.join(self._endpoint, "__api__/v1/users", params["guid"])
response = self._session.get(endpoint)
return User(**response.json())

def get_current_user(self) -> Response:
endpoint = os.path.join(self._endpoint, "__api__/v1/user")
return self._session.get(endpoint)
# Otherwise, perform a normal search.
return next(iter(self.find(params)), None)
21 changes: 0 additions & 21 deletions src/posit/connect/users_test.py
Original file line number Diff line number Diff line change
@@ -1,21 +0,0 @@
from unittest.mock import Mock

from .users import Users


class TestUsers:
def test_get_user(self):
session = Mock()
session.get = Mock(return_value={})
users = Users(endpoint="http://foo.bar/", session=session)
response = users.get_user(user_id="foo")
assert response == {}
session.get.assert_called_once_with("http://foo.bar/__api__/v1/users/foo")

def test_get_current_user(self):
session = Mock()
session.get = Mock(return_value={})
users = Users(endpoint="http://foo.bar/", session=session)
response = users.get_current_user()
assert response == {}
session.get.assert_called_once_with("http://foo.bar/__api__/v1/user")
12 changes: 8 additions & 4 deletions tinkering.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from posit.connect.client import Client
from posit.connect import make_client

client = Client()
res = client.users.get_current_user()
print(res.json())
client = make_client()
for user in client.users.find({"username": "aaron"}):
print(user)

print(client.users.find_one())

print(client.users.find_one({"guid": "f155520a-ca2e-4084-b0a0-12120b7d1add"}))
Loading