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: switch User and Content to inherit dict, add update method for User #94

Merged
merged 8 commits into from
Mar 18, 2024

Conversation

nealrichardson
Copy link
Collaborator

To help us move the discussion forward. I switched User to inherit from dict to see how that works, including adding an update method and wiring that up. I first tried UserDict but it messed up the sort order of the columns in the DataFrame version, and googling a bit, it sounded like it wasn't so bad to inherit from dict anymore, so I figured I'd try that and see how it went. Summary of what I found so far, as reflected in the test suite:

  • ✅ Forwards and backwards compatibility (i.e., what happens if the API returns more or fewer fields than the class definition knows about) all works fine. If new fields appear and you haven't defined propertys for them, it doesn't break, and you can still get at them via [. Missing fields also are fine, at least as long as you don't access them (you'd get a KeyError).
  • update() method works. I was worried from that blog post that you wouldn't be able to define a new one, but it was fine. I did not try setattr, and my guess is that we'd want to disable that anyway (but that's a discussion for another time).
  • 🤔 pd.DataFrame(list of Users) works fine since it is a dict still, but: in adding the session and url to the object in order to implement update(), those now show up in the data frame. AFAIK there's no way to hide them somewhere else, it's just a dict.
  • 🤔 @propertys work to access the data, but the type hints don't enforce anything. I didn't add in the datetime parsing, just to see what happens, and the methods happily return the unparsed strings. Not strictly a problem, but worth keeping in mind.
  • Other consideration related to the last two: we could add an __init__ method to do things like parse timestamps. As we discussed before, there are tradeoffs to doing this eagerly vs. on demand. Alternatively, if we kept it lazy (my preference), we could add a to_pandas function or something that would parse them when you do that conversion, and leave the attribute parsing to the property getters.

Copy link

github-actions bot commented Mar 15, 2024

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
487 391 80% 80% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
src/posit/connect/content.py 72% 🟢
src/posit/connect/users.py 83% 🟢
TOTAL 78% 🟢

updated for commit: f7c606a by action🐍

@nealrichardson nealrichardson changed the title feat: switch User to inherit dict, add update method feat: switch User and Content to inherit dict, add update method for User Mar 16, 2024
@nealrichardson nealrichardson marked this pull request as ready for review March 16, 2024 19:12
@nealrichardson
Copy link
Collaborator Author

I've updated both User and ContentItem to inherit from dict and added all of the property accessors. I also revised the update() method for User to enumerate (and type) the fields you can update. I think I should stop here and make more explicit followups for the next steps (various TODOs left in the code, for one), rather than roll more into this PR.

One issue I could use help with: mypy does not like this change.

@nealrichardson
Copy link
Collaborator Author

Ok @tdstein I think this is done (enough) and we should merge so we can unblock other work.

@tdstein
Copy link
Collaborator

tdstein commented Mar 18, 2024

🤔 pd.DataFrame(list of Users) works fine since it is a dict still, but: in adding the session and url to the object in order to implement update(), those now show up in the data frame. AFAIK there's no way to hide them somewhere else, it's just a dict.

Can we implement an __init__ method in Resource as follows? I haven't tried it yet.

def __init__(self, session: requests.Session, url: str, **kwargs):
    self._session = session
    self._url = url
    super().__init__(**kwargs)

@tdstein
Copy link
Collaborator

tdstein commented Mar 18, 2024

Other consideration related to the last two: we could add an init method to do things like parse timestamps. As we discussed before, there are tradeoffs to doing this eagerly vs. on demand. Alternatively, if we kept it lazy (my preference), we could add a to_pandas function or something that would parse them when you do that conversion, and leave the attribute parsing to the property getters.

Let's go ahead and defer this decision until later. I'm satisfied that pandas.DataFrame(users) works. 😄

Comment on lines +63 to +97
def _update(self, body):
self.get("session").patch(self.get("url"), json=body)
# If the request is successful, update the local object
super().update(body)
# TODO(#99): that patch request returns a payload on success,
# so we should instead update the local object with that payload
# (includes updated_time)

def update( # type: ignore
self,
# Not all properties are settable, so we enumerate them here
# (also for type-hinting purposes)
email: Optional[str] = None,
username: Optional[str] = None,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
user_role: Optional[str] = None,
# TODO(#100): in the API, this goes via POST /v1/users/{guid}/lock
# accept it here and make that request? Or add a .lock() method?
# locked: Optional[bool] = None,
) -> None:
kwargs = {}
if email is not None:
kwargs["email"] = email
if username is not None:
kwargs["username"] = username
if first_name is not None:
kwargs["first_name"] = first_name
if last_name is not None:
kwargs["last_name"] = last_name
if user_role is not None:
kwargs["user_role"] = user_role
# if locked is not None:
# kwargs["locked"] = locked
self._update(kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks good. But should we enforce these logical constraints or defer this behavior to the server? In other words, how do we handle behavioral changes to the PATCH endpoint between versions of Connect?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Suppose a future version of Connect adds a new field you can PATCH to update. We would add it here explicitly, and users would need to upgrade the SDK to the latest to access it. That seems fine.

Suppose a future version of Connect changed it so that user_role didn't exist, and instead you provided a permissions dict of named booleans. We would add a check here for the version of the Connect server, and if you provided user_role to a version of Connect that only supported permissions, map our understanding of our current roles to those permissions bundles (or error). We could also do the reverse and map known permission bundles back to user_role (or error).

So I think this is ok, or at least we have options.

Copy link
Collaborator

@tdstein tdstein left a comment

Choose a reason for hiding this comment

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

Ok @tdstein I think this is done (enough) and we should merge so we can unblock other work.

Agreed. Let's merge and continue iterating.

@nealrichardson
Copy link
Collaborator Author

🤔 pd.DataFrame(list of Users) works fine since it is a dict still, but: in adding the session and url to the object in order to implement update(), those now show up in the data frame. AFAIK there's no way to hide them somewhere else, it's just a dict.

Can we implement an __init__ method in Resource as follows? I haven't tried it yet.

def __init__(self, session: requests.Session, url: str, **kwargs):
    self._session = session
    self._url = url
    super().__init__(**kwargs)

I don't think that will succeed in hiding the session and url from the dict itself since it inherits dict, but we can experiment more. Let's merge this and then your other PR, and then we can tinker with the base class.

@nealrichardson nealrichardson merged commit df0630b into main Mar 18, 2024
7 checks passed
@nealrichardson nealrichardson deleted the try-using-dicts branch March 18, 2024 15:10
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.

None yet

2 participants