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

Coming soon: API update for Streamlit query params #7665

Closed
sfc-gh-jcarroll opened this issue Nov 2, 2023 · 13 comments
Closed

Coming soon: API update for Streamlit query params #7665

sfc-gh-jcarroll opened this issue Nov 2, 2023 · 13 comments
Labels
area:utilities feature:query-params type:enhancement Requests for feature enhancements or new features

Comments

@sfc-gh-jcarroll
Copy link
Collaborator

sfc-gh-jcarroll commented Nov 2, 2023

Hi all, an FYI that we’re working on an upgrade to Streamlit’s query params API. I’m sharing about it today to get feedback on the new API and help assess whether to release it as experimental_ or go directly to a stable API. Take a look and let us know what you think!

Current state

Query params support is a fairly popular feature for providing some state or input about an app as part of the URL, making it easier to share with users or collaborators in a given state. Today it’s supported in Streamlit with two methods, experimental_get_query_params and experimental_set_query_params.

Both methods treat the query params as a single blob and require the app developer to track and manage the state (in case of multiple params) and handle list conversion.

st.experimental_set_query_params(
    show_map=True,
    selected=["asia", "america"],
)
# Output URL: localhost:8501/?show_map=True&selected=asia&selected=america

# Now return value if I call get
st.experimental_get_query_params()
{"show_map": ["True"], "selected": ["asia", "america"]}

Planned API update

Our plan is to move query params to a dict-like API similar to st.session_state, where Streamlit tracks the state of all query params and app developers can interact with them independently. For query params with only a single value, this also means the developer doesn’t need to worry about list conversion.

st.query_params["show_map"] = True
st.query_params["selected"] = ["asia", "america"]
# Same output URL as above

# Note we convert everything to str return type
st.query_params["show_map"] # returns "True"
st.query_params.show_map # also works, returns "True"

Handling lists and multiple values

A key concern for query parameters is handling multiple values. For normal get / retrieval calls on query_params, Streamlit will return only the last value set for the given key, whether in the URL directly via Streamlit API. To retrieve a list of all set values, developers can call st.query_params.get_all(key). This approach is typical of several other python web API frameworks such as tornado and flask.

# URL: localhost:8501/?show_map=True&selected=asia&selected=america

st.query_params["selected"]
# "america"

st.query_params.get_all("selected")
# ["asia", "america"]

st.query_params.get_all("show_map")
# ["True"]

Other improvements

We’ll also be making a few quality improvements to the existing support and making sure less common cases like setting an empty string / blank value are supported.

What do you think?

Let us know your feedback about the changes. Our current plan is to release this minimal version, which simplifies the API for typical use cases and leaves room to extend to new features in the future such as better handling for type conversion, or binding query params to certain session_state keys or widget values.

Your input can help impact whether we release this immediately as st.query_params or do an st.experimental_query_params to see more usage of the new API before finalizing it.

Thank you!

@sfc-gh-jcarroll sfc-gh-jcarroll added the type:enhancement Requests for feature enhancements or new features label Nov 2, 2023
Copy link

github-actions bot commented Nov 2, 2023

To help Streamlit prioritize this feature, react with a 👍 (thumbs up emoji) to the initial post.

Your vote helps us identify which enhancements matter most to our users.

Visits

@edsaac
Copy link

edsaac commented Nov 3, 2023

Looks awesome 👍

The only thing I dislike about the query params API is that the embed query options are hidden and not sure what's the benefit of that. Is the plan to keep it that way?

# Return new query params dict, but without embed, embed_options query params
return util.exclude_key_query_params(
parse.parse_qs(ctx.query_string, keep_blank_values=True),
keys_to_exclude=EMBED_QUERY_PARAMS_KEYS,
)

@sfc-gh-jcarroll
Copy link
Collaborator Author

Thanks @edsaac! We have the embed exclusion so that the site embedding an app has control over how it is rendered. This includes platforms that embed apps. If we exposed the control directly to the app, it could lead to inconsistent or unexpected embeddings (whether intentionally or not).

I'm curious if you had a specific use case in mind for this? Current plan of record is to keep the embed options hidden with the new API, but interested to hear if there's a reason to do something different. Thanks!

@arnaudmiribel
Copy link
Collaborator

arnaudmiribel commented Nov 6, 2023

Thanks @sfc-gh-jcarroll for the proposal! I think it makes sense to make query params API more consistent with st.session_state. Few questions:

  1. What's the perk of using selected=america&selected=asia instead of selected=america,asia? Consistency with other frameworks is legit, but I'm assuming there's also some good reason I don't see.
  2. I think similarly, if get_all also works for a single value, why would I ever use get instead of get_all?
  3. Actually a pretty popular use-case is to have both session state and query params being synced: see sharing contextual apps. I wonder if this new API helps/hurts that use-case - and if there's more we can think about to help it (can also be a non-goal). The following could almost work (?) if not for type conversions and get_all logics.
# Read
st.session_state |= st.query_params

# ...
# All my app, with explicit `key`s defined when needed
# ... 

# Write
st.query_params |= st.session_state

An extreme case could be something like st.selectbox("foo", key="selected", query_param=True) which then always reads from/writes to the URL for specifically chosen widgets, or some st.set_page_config(..., synced_keys=["foo", "bar"])

@sfc-gh-jcarroll
Copy link
Collaborator Author

Thanks @arnaudmiribel !

  1. Yeah the consistency is a big reason. It means , doesn't need to be a reserved value in the query params. And if you want to use the , approach in your app for some reason, you can easily do val = st.query_params["foo"].split(",") to get that behavior. But the inverse isn't true - if we used the , behavior by default it'd be very difficult to have an app use the other approach.

  2. From what I've seen, the most common pattern in an app is to expect just one value to be set for the query param. In that case, it's much more convenient to use get. Your app can't guarantee that a user will not do something unexpected like supply multiple values directly in the URL. So in that case, if your app uses get_all(), you also always need to confirm that there's only one value and handle the case where a user does something malicious. It creates more room for apps with unexpected exploits and more complex code. Also, lots of evidence that this is a good idea from almost every other web server supporting this mode 😄

  3. I agree about the session_state linkage being very common and useful. I believe this direction will be a step towards making that easier. But I would also like to do a follow-on for more explicit binding or key syncing similar to what you mentioned, since I see it pretty commonly too and it seems very useful (by the way, I think someone could write a common function to do this with the new API pretty easily). I didn't see any feature request for this yet in the issues but would welcome one to help track the demand more explicitly. Also if you see evidence that this design makes that story worse, please do share.

@arnaudmiribel
Copy link
Collaborator

Thanks for the context. Makes sense! As for the feature request, I would guess the closest is that one #302.

@edsaac
Copy link

edsaac commented Nov 13, 2023

@sfc-gh-jcarroll my use case for checking if the app is embed is just to show a simpler layout. For example, if embed it won't render st.toasts, hide the sidebar, etc. For a multipage app with a sizable computation load, I would just show a description of the app if embed and defer its full capabilities for the fullscreen version.

In case someone finds that useful, this is how I check if the app is embed. Ofc it will need to be updated after the new better API rolls out.

def is_embed():
    from streamlit.runtime.scriptrunner import get_script_run_ctx
    ctx = get_script_run_ctx()
    query_params = parse.parse_qs(ctx.query_string)
    return True if query_params.get("embed") else False

IS_APP_EMBED = is_embed()

@sfc-gh-jcarroll
Copy link
Collaborator Author

sfc-gh-jcarroll commented Nov 13, 2023

Nice, thanks @edsaac - Cool scenario and it makes sense. Based on your feedback we talked about it and agreed to make the embed param visible (read only) in the new API. EDIT: see comment below. Want to support this but most likely through another approach.

@sfc-gh-jcarroll
Copy link
Collaborator Author

Hey @edsaac sorry for the bait and switch - after thinking about this some more, I think it's preferable to keep the current hiding of embed params. One specific use case in mind is if an app wants to do some if st.query_params or for param in st.query_params: it might lead to weird behavior if the embed is shown unexpectedly. Note that some platforms also use embedding, for example huggingface spaces. So it might be weird in that case if the param always showed up for apps there.

I think the use case is totally legit. Wondering about exposing another simple function for this, maybe some st.platform.is_embedded() which returns true/false or something like that. If you have thoughts LMK.

@edsaac
Copy link

edsaac commented Nov 16, 2023

That would be great! It's basically what I currently do with that small is_embed() function.

@bartbroere
Copy link

Hi all! I just left a comment on one of the pull requests, but I would also like to suggest it here so it may be discussed: Could we mimick accessing query params the way Flask decided to do it, with a flat=False / flat=True parameter?

I'm currently already using the streamlit.experimental_get_query_params feature, and I'm hoping to continue to be able to predictably get a list for one of my GET parameters, even if there's only one value in them. If there's a different way to do that, that's also fine with me, but I would like to be able to avoid adding isinstance checks to each place where I use GET parameters.

@sfc-gh-jcarroll
Copy link
Collaborator Author

Thanks @bartbroere, I think we have it covered with the get_all() method which will always return a list of values (including 0 or 1 element list). It should work as below. Let me know if that seems to meet your need or if you see issues.

# URL: localhost:8501/?show_map=True&selected=asia&selected=america

st.query_params.get_all("selected")
# ["asia", "america"]

st.query_params.get_all("show_map")
# ["True"]

@sfc-gh-jcarroll
Copy link
Collaborator Author

This feature has been merged #7774 and will be included in the next Streamlit release, 1.30 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:utilities feature:query-params type:enhancement Requests for feature enhancements or new features
Projects
None yet
Development

No branches or pull requests

4 participants