-
Notifications
You must be signed in to change notification settings - Fork 68
Ms/user access management #138
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
Conversation
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.
Going to take a second look tommorow when I am fresh!
I might ask Greg to also take a look, because I think experimental
mode is a big change and we should be cautious in how we roll it out in terms of the API
also I think Grant/Collin might be helpful in providing some context of how the invite api ought to be used.
After speaking with Grant, we decided to remove the ability to query and revoke active invites since this might behave unexpectedly. He also said we need to remove the old |
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.
will continue after stand up
raise KeyError( | ||
f"Expected field values for {relationship.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.
Still wrapping my head around all this, but are we sure we want to raise an error here? What if we want to do lazy loading of cached relationships?
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.
If we need that in the future we can make the change. I think making this more strict is better.
labelbox/schema/role.py
Outdated
def __new__(cls, client): | ||
if cls._instance is None: | ||
cls._instance = super(Roles, cls).__new__(cls) | ||
query_str = """query GetAvailableUserRolesPyApi { roles { id name } }""" | ||
res = client.execute(query_str) | ||
valid_roles = set() | ||
for result in res['roles']: | ||
_name = result['name'].upper().replace(' ', '_') | ||
result['name'] = _name | ||
setattr(cls._instance, _name, Role(client, result)) | ||
valid_roles.add(_name) | ||
cls._instance.valid_roles = valid_roles | ||
cls._instance.index = 0 | ||
return cls._instance |
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.
In general I'm not a huge fan of overriding __new__
. Imho the code gets overly complicated. If we want to make them singletons, could we instead declare the roles as lazy initalized module vars - sth like here https://github.com/Labelbox/python-monorepo/blob/6c6bfebb68fc6f876c3abf29c9ce727a6c5744dc/services/prediction_import/prediction_import/db/client.py#L357
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.
This is a pretty standard singleton pattern. The code snippet you provided is much simpler but also because it is doing much less. This is going to basically look the same except swap new for init and add a new module level variable which feels like a lateral move rather than an improvement.
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 lean more towards, structs/class should be dumb containers of data than locations of business logic.
In that sense I think you could seperate fetching GetAvailableUserRolesPyApi
logic from the class __new__
and move it out into a static method or function, imho it doesn make reading the class a little trickier.
class RoleAccessor(self):
def __init__(self):
self.roles = get_The_roles(client)
or some other means to break up the logic
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 changed it to this for now.
labelbox-python/labelbox/schema/role.py
Lines 8 to 52 in e6e4dd6
def _get_roles(client): | |
query_str = """query GetAvailableUserRolesPyApi { roles { id name } }""" | |
if not hasattr(_get_roles, 'roles'): | |
_get_roles.roles = res = client.execute(query_str) | |
return _get_roles.roles | |
class Roles: | |
""" | |
Object that manages org and user roles | |
>>> roles = client.get_roles() | |
>>> roles.valid_roles # lists all valid roles | |
>>> roles['ADMIN'] # returns the admin Role | |
""" | |
def __init__(self, client): | |
res = _get_roles(client) | |
valid_roles = set() | |
for result in res['roles']: | |
_name = result['name'].upper().replace(' ', '_') | |
result['name'] = _name | |
setattr(self, _name, Role(client, result)) | |
valid_roles.add(_name) | |
self.valid_roles = valid_roles | |
def __repr__(self): | |
return str({k: getattr(self, k) for k in self.valid_roles}) | |
def __getitem__(self, name): | |
name = name.replace(' ', '_').upper() | |
if name not in self.valid_roles: | |
raise ValueError( | |
f"No role named {name} exists. Valid names are one of {self.valid_roles}" | |
) | |
return getattr(self, name) | |
def __iter__(self): | |
self.key_iter = iter(self.valid_roles) | |
return self | |
def __next__(self): | |
key = next(self.key_iter) | |
return getattr(self, key) |
labelbox/schema/role.py
Outdated
def __new__(cls, client): | ||
if cls._instance is None: | ||
cls._instance = super(Roles, cls).__new__(cls) | ||
query_str = """query GetAvailableUserRolesPyApi { roles { id name } }""" | ||
res = client.execute(query_str) | ||
valid_roles = set() | ||
for result in res['roles']: | ||
_name = result['name'].upper().replace(' ', '_') | ||
result['name'] = _name | ||
setattr(cls._instance, _name, Role(client, result)) | ||
valid_roles.add(_name) | ||
cls._instance.valid_roles = valid_roles | ||
cls._instance.index = 0 | ||
return cls._instance |
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 lean more towards, structs/class should be dumb containers of data than locations of business logic.
In that sense I think you could seperate fetching GetAvailableUserRolesPyApi
logic from the class __new__
and move it out into a static method or function, imho it doesn make reading the class a little trickier.
class RoleAccessor(self):
def __init__(self):
self.roles = get_The_roles(client)
or some other means to break up the logic
|
||
@beta_endpoint | ||
def cancel_invite(client, invite_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.
You could maybe turn this comments into application logic through introducing a dummy decorator that annotates like test method only or something like that.
or like throw if pytest is not running:
https://stackoverflow.com/questions/25188119/test-if-code-is-executed-from-within-a-py-test-session
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 main point of the comment is to make it so that users don't copy this into their own application. I don't think there is a risk of someone importing from the conftest outside of the tests.
|
||
@pytest.fixture | ||
def organization(client): | ||
# Must have at least one seat open in your org to run these tests |
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.
is this gonna make int. tests flakier?
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.
No it clears invites before and after (only @labelbox.com invites so users don't accidentally mess up anything). Our test account is only used for testing so there shouldn't be any problem here. This will only matter if you are testing locally and don't have any open seats.
labelbox/client.py
Outdated
@@ -208,9 +213,9 @@ def check_errors(keywords, *path): | |||
if internal_server_error is not None: | |||
message = internal_server_error.get("message") | |||
|
|||
if message.startswith("Syntax Error"): | |||
if message.startswith("Syntax Error") or message.startswith( | |||
"Invite(s) cannot be sent"): |
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.
This feels really hard-coded in a way. I would expect the basic execute
function to maybe catch top level errors, but not super specific errors from particular queries.
also InvalidQueryError
to me implies that the query was malformed as opposed to something about their user limits
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.
This is because the backend just throws a 500 and there is no way to handle this without parsing the message (see the comment just above this.
Server side validation is one reason we raise this exception:
labelbox-python/labelbox/exceptions.py
Lines 63 to 65 in 56a366c
""" Indicates a malconstructed or unsupported query (either by GraphQL in | |
general or by Labelbox specifically). This can be the result of either client | |
or server side query validation. """ |
I don't like this pattern either but I don't think fixing this is within the scope of this delivery.
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.
aite, one small nit is that startswith
takes a tuple I believe
message.startsiwh(("message1, "message"))
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.
Fixed.
from typing import Any, Callable, Dict, List, Optional, Tuple, Type | ||
|
||
from typing import TYPE_CHECKING | ||
if TYPE_CHECKING: |
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.
Is adding TYPE_CHECKING
check standard?
Do we add it for compatibility or performance reasons?
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 is to avoid circular imports. There is not really a great solution here.
return _ROLES | ||
|
||
|
||
def format_role(name: 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.
jw, do we even need to format_roles now that we are not emulating enum behaavior?
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.
Idk I don't like spaces and inconsistent casing in key names. It makes it harder to use.
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.
@gszpak it all looks good to me, you have any follow up thoughts?
PLAT-1224
The purpose of this pr is to allow customers to manage organization invites and user permissions from the SDK. The following functionality was added:
update / revoke invitequery for active invites