Support for the Grist SCIM apis.¶
Grist introduced support for the SCIM standard apis for managing users, back in version 1.3.2. Even if the Grist SCIM apis are still at an early stage of developement, we decided to mirror them in Pygrister. A fair warning: both the Grist apis and Pygrister’s rendition are experimental and likely to change.
This is also a provisional doc page for our Pygrister SCIM support, that leaves quite a few points unresolved: we will update this page as the Grist apis (and our own understanding) will evolve.
You may learn about the Grist SCIM apis browsing their official documentation.
SCIM is not for everyone.¶
First of all, SCIM is not even enabled on the “regular” SaaS Grist (that
is, www.getgrist.com). If you want to try out SCIM, you must set up a local
installation and enable SCIM (set GRIST_ENABLE_SCIM
in your environment).
(Note: you will find a new SCIM section in your Api console, but you can’t actually place a call if you are on SaaS Grist.)
Be careful! If you already run your own Grist service, please do not run the Pygrister test suite against the same instance that you use for everyday’s work: always set up a separate, disposable container for testing. This is because our test suite will create many users, that you can’t easily delete afterwards.
Sometimes, SCIM is different.¶
While testing the Grist SCIM apis ourselves, we found quite a few rough edges: sometimes we tried to accomodate, sometimes we just couldn’t figure it out. For instance, and in no particular order:
the apis will accept user IDs as integers, return them as strings: we always use integers;
when creating or updating a user, protocol for providing emails and pictures is rather convoluted and support multiple instances: in reality, Grist will only store the “primary” email/pic. It’s easier in Pygrister: you just provide a list of strings for both emails and pics: the first one is intended as the primary (then again, only the first item will be used by Grist anyway);
the DELETE endpoint will only return a status code, with no payload. This is an exception (perhaps an oversight?) to the behaviour of Grist apis, which always return some kind of content. The corresponding api in Pygrister will return
None
, like every otherdelete_*
function;we couldn’t make bulk operations work: the api call itself goes through, but then every single operation will fail because the “path” component appears to be wrong. Maybe it’s just us… if anyone figures this our, please let us know! (Also, please note that Pygrister’s
bulk_users
will return a list of the status codes for every single operation, to simplify the underlying Grist api. To check the “real” response, inspectgrist.resp_content
as usual.)we couldn’t make filters work in the getUsers endpoint, while they seem to work just fine in the search case. See also the relevant code in our test suite.
If you are interested in testing the Grist SCIM apis with Pygrister, you are most welcome: please be advised that it might be difficult to tell whether a problem is with the Grist apis or Pygrister’s code. You should always double-check you call with the Grist Api console, before filing a bug report to the wrong place!
Wait, did you say pagination?!¶
Two of the new Grist SCIM endpoints (namely, getUsers and searchUsers) support pagination: you also pass a starting point and a number of users to be retrieved with one single call.
While these are the only “paginated” endpoints in the Grist apis right now (and, realistically, the number of users is rarely high enough to need this), we cannot exclude that there are plans to introduce pagination in other places at some point.
Thus, we made a little extra effort in supporting pagination in a smoother, more pythonic way. With any luck, the mechanism will also adapt to future paginated apis, if they arrive. However, keep in mind that this part is still very unstable and experimental, and may change in the future.
The rule is: every Grist endpoint (say, fooapi
) that support pagination
always has two corrisponding functions in the GristApi
class:
a
GristApi.fooapi_raw
function, that callsfooapi
in the traditional way, without any “fancy” pagination. You just submit your starting point and number of items to be retrieved, and the function will return the usualstatus_code, result
tuple, just like all the other Pygrister apis. If you then want the next chunk of results, you’ll have to make another call tofooapi_raw
, submitting the next starting point, and so on. Please note: allGristApi
functions ending in*_raw
will always return the response “as it is” from the Grist api call (ie, a dictionary). This is an exception to the usual Pygrister naming convention: for instance alist_fooapi_raw
function will return a single dictionary, not a list.a
GristApi.fooapi
function, that deals with the api the “fancy” way, with auto-pagination. These functions will not return the usualstatus_code, result
tuple, but instead a Python iterable object, that you can transverse like a normal Python iterable. Each time you callnext()
on it, the iterable will in turn post the api call for you, and return thestatus_code, result
tuple; the internal indexes will be automatically updated.
An example will help clarify. GristApi.list_users
is a “paginated” api
(and list_users_raw
is the corresponding traditional function).
Let’s see it at work:
>>> from pygrister.api import GristApi
>>> grist = GristApi(...)
>>> userlist = grist.list_users(start=1, chunk=5) # (1)
>>> status_code, result = next(userlist) # (2)
>>> status_code, result = next(userlist) # (3)
>>> # and so on, until...
>>> status_code, result = next(userlist) # (4)
...
StopIteration
In (1)
, we call the GristApi
“paginated” function. Note that at,
this point, no actual api call has been placed yet: userlist
is just
an “empty” iterable. Then, in (2)
, we start iterating. Now the
first api call is posted, and the result is retrieved. Hopefully, the
status code will be 200 and result
will have the first batch of 5
users. At the next iteration, in (3)
, a new api call will be placed,
with the updated index, retrieving the next 5 users, and so on.
When there are no more users to retrieve, a StopIteration
will be raised
(this is the normal way in Python).
Of course, you don’t have to keep calling next
. As with any regular
Python iterable, you want to use a for
loop:
>>> userlist = grist.list_users(start=1, chunk=5)
>>> for status_code, result in userlist:
... print(status_code) # or whatever
Pretty neat, right? At every step of the loop, the api call will be posted and the result retrieved. But wait, there’s more!
After the first call has been posted, the iterable will have a __len__
attribute storing the total number of items:
>>> userlist = grist.list_users(start=1, chunk=5)
>>> len(userlist) # we can't know just yet
0
>>> st, res = next(userlist)
>>> len(userlist) # now this is the total number we are going to retrieve
42
You can still maintain control of the fine-tuning, even when using the
iterable object: the attributes index
and items
have the current
index and the number of items to retrieve, and you may change them as you
iterate. For example, this trick will repeat the last item of the previous
chunk (useful sometimes in real-life pagination):
>>> userlist = grist.list_users(start=1, chunk=5)
>>> for status_code, result in userlist:
... print([i['id'] for i in result])
... userlist.index -= 1 # move the index back one
...
[1, 2, 3, 4, 5]
[5, 6, 7, 8, 9]
[9, 10]
Another interesting feature to keep in mind: the iterable object is just a
thin wrapper, but the actual api call is still managed by the GristApi
instance as usual. This means that all the usual goodies are still available,
just like for any other api call. For instance, if you get a bad status code
while iterating, you can still inspect
the GristApi
instance to find out
what happened:
>>> userlist = grist.list_users(start=1, chunk=5)
>>> st, res = next(userlist)
>>> # now, say the server crashes...
>>> st, res = next(userlist)
...
HTTPError
>>> print(grist.inspect()) # GristApi will know!
Of course, for now we have only 2 “paginated” apis (list_users
and
search_users
, with the corresponding list_users_raw
and
search_users_raw
) and they both deal with the niche SCIM interface,
so all of this probably won’t do you any good in everyday life… but maybe
in the future!
Finally, there is still one oddity (a bug, perhaps?) in the Grist apis
that you should be aware of. When you pass an out-of-range index to a
“paginated” api, you will still retrieve the first items as if nothing.
You can test this in Pygrister too, using the *_raw
function that mirrors
the original api behaviour:
>>> st, res = grist.list_users_raw(start=1, chunk=5) # the first 5 users
>>> st, res = grist.list_users_raw(start=100000, chunk=5) # still the first 5 users!
This is annoying: when you iterate manually, you risk cycling over and over, because you’ll never get an empty set of results. Keep an eye on the total number of items to know when to stop. Of course, our fancy iterable object already keeps track for you behind the scenes, so you won’t have this problem if you use it instead.