Repository: streamlit/streamlit

Issue:
Remove Streamlit Mapbox API key
_Edit: apparently this is _not_ how Mapbox works. See Update section below._

If I understand correctly how Mapbox works, I think we don't need an API key by default since Mapbox throttles usage of their maps based on IP address (which is user-specific).

Can we just remove the default API key? **And we'd need to allow users to enter their own API key, of course.** 

---

Update:

DeckGL depends on Mapbox to show maps. Mapbox requires developers to have a token. Up to now, Streamlit hard-coded a token to make people's lives easier. The problem is that for the past few months we've started going over the free QPS limit of that token, and so we've been paying for everyone's usage of maps.

So let's fix this!

Proposal:
- Whenever a users tries to use DeckGL and they didn't provide a token (this is done via the config file), we'll show an error explaining what they need to do.
- We'll update our docs to make this abundantly clear.
- To make the `streamlit hello` experience nice, the map in that app will continue to use our own paid token. But we'll take steps to make sure only that app can use that token, as much as possible.


Discussion:

Resolving patch:
diff --git a/lib/streamlit/config.py b/lib/streamlit/config.py
--- a/lib/streamlit/config.py
+++ b/lib/streamlit/config.py
@@ -537,11 +537,9 @@ def _browser_server_port():
 _create_option(
     "mapbox.token",
     description="""Configure Streamlit to use a custom Mapbox
-                token for elements like st.deck_gl_chart and st.map. If you
-                don't do this you'll be using Streamlit's own token,
-                which has limitations and is not guaranteed to always work.
+                token for elements like st.deck_gl_chart and st.map.
                 To get a token for yourself, create an account at
-                https://mapbox.com. It's free! (for moderate usage levels)""",
+                https://mapbox.com. It's free (for moderate usage levels)!""",
     default_val="",
 )
 


Test patch:
diff --git a/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts b/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts
--- a/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts
+++ b/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts
@@ -15,11 +15,15 @@
  * limitations under the License.
  */
 
-import { MapboxToken, TOKENS_URL } from "hocs/withMapboxToken/MapboxToken"
+import axios from "axios"
 import { SessionInfo } from "lib/SessionInfo"
-import fetchMock from "fetch-mock"
+import AxiosMockAdapter from "axios-mock-adapter"
+import { MapboxToken, TOKENS_URL } from "hocs/withMapboxToken/MapboxToken"
 
-function setSessionInfoWithMapboxToken(userMapboxToken: string): void {
+function setSessionInfo(
+  userMapboxToken = "",
+  commandLine = "streamlit hello"
+): void {
   SessionInfo.current = new SessionInfo({
     sessionId: "mockSessionId",
     streamlitVersion: "sv",
@@ -27,31 +31,38 @@ function setSessionInfoWithMapboxToken(userMapboxToken: string): void {
     installationId: "iid",
     authorEmail: "ae",
     maxCachedMessageAge: 2,
-    commandLine: "cl",
-    userMapboxToken: userMapboxToken,
+    commandLine,
+    userMapboxToken,
   })
 }
 
 describe("MapboxToken", () => {
+  let axiosMock: AxiosMockAdapter
+
   beforeEach(() => {
-    setSessionInfoWithMapboxToken("")
+    window.location.hostname = "localhost"
+    axiosMock = new AxiosMockAdapter(axios)
+    setSessionInfo("")
   })
 
   afterEach(() => {
-    SessionInfo["singleton"] = undefined
+    axiosMock.restore()
     MapboxToken["token"] = undefined
-    fetchMock.restore()
+    MapboxToken["commandLine"] = undefined
+    SessionInfo["singleton"] = undefined
   })
 
   test("Returns cached token if defined", async () => {
     MapboxToken["token"] = "cached"
+    MapboxToken["commandLine"] = "streamlit hello"
+
     await expect(MapboxToken.get()).resolves.toEqual("cached")
   })
 
   test("Returns userMapboxToken if non-empty", async () => {
     const userToken = "nonEmptyToken"
 
-    setSessionInfoWithMapboxToken(userToken)
+    setSessionInfo(userToken)
     await expect(MapboxToken.get()).resolves.toEqual(userToken)
 
     // The token should also be cached.
@@ -61,7 +72,8 @@ describe("MapboxToken", () => {
   test("Fetches remote token if userMapboxToken is empty", async () => {
     const remoteToken = "remoteMapboxToken"
 
-    fetchMock.get(TOKENS_URL, { mapbox: remoteToken })
+    axiosMock.onGet(TOKENS_URL).reply(200, { "mapbox-localhost": remoteToken })
+
     await expect(MapboxToken.get()).resolves.toEqual(remoteToken)
 
     // The token should also be cached.
@@ -69,7 +81,7 @@ describe("MapboxToken", () => {
   })
 
   test("Errors if remote token is missing", async () => {
-    fetchMock.get(TOKENS_URL, { ohNo: "noTokenHere" })
+    axiosMock.onGet(TOKENS_URL).replyOnce(200, { ohNo: "noTokenHere" })
 
     await expect(MapboxToken.get()).rejects.toEqual(
       new Error(`Missing token "mapbox" (${TOKENS_URL})`)
@@ -78,13 +90,40 @@ describe("MapboxToken", () => {
     // No cached token after failure.
     expect(MapboxToken["token"]).toBeUndefined()
 
-    fetchMock.restore()
-    fetchMock.get(TOKENS_URL, 404)
+    axiosMock.onGet(TOKENS_URL).replyOnce(404, {})
     await expect(MapboxToken.get()).rejects.toEqual(
-      new Error(`Bad status: 404 (${TOKENS_URL})`)
+      new Error(`Request failed with status code 404 (${TOKENS_URL})`)
     )
 
     // No cached token after failure.
     expect(MapboxToken["token"]).toBeUndefined()
   })
+
+  it("Errors if not localhost and missing token", async () => {
+    delete window.location
+    window.location = { hostname: "http://streamlit.io" } as Location
+    setSessionInfo("")
+
+    await expect(MapboxToken.get()).rejects.toThrow("No Mapbox token provided")
+  })
+
+  it("Errors if not hello.py and missing token", async () => {
+    setSessionInfo("", "streamlit run example.py")
+
+    await expect(MapboxToken.get()).rejects.toThrow("No Mapbox token provided")
+  })
+
+  it("Should reload token if command line has changed", async () => {
+    setSessionInfo()
+
+    const remoteToken = "remoteMapboxToken"
+
+    axiosMock.onGet(TOKENS_URL).reply(200, { "mapbox-localhost": remoteToken })
+
+    await expect(MapboxToken.get()).resolves.toEqual(remoteToken)
+
+    setSessionInfo("password", "streamlit run test.py")
+
+    await expect(MapboxToken.get()).resolves.toEqual("password")
+  })
 })
diff --git a/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx b/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx
--- a/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx
+++ b/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx
@@ -18,6 +18,7 @@
 import React from "react"
 import { shallow } from "enzyme"
 import { MapboxToken } from "./MapboxToken"
+import { SessionInfo } from "lib/SessionInfo"
 
 import withMapboxToken from "./withMapboxToken"
 
@@ -35,23 +36,41 @@ function waitOneTick(): Promise<void> {
 }
 
 describe("withMapboxToken", () => {
-  function getProps(): any {
+  const token = "mockToken"
+  const commandLine = "streamlit run test.py"
+
+  function getProps(): object {
     return { label: "label" }
   }
 
+  beforeAll(() => {
+    SessionInfo.current = new SessionInfo({
+      sessionId: "mockSessionId",
+      streamlitVersion: "sv",
+      pythonVersion: "pv",
+      installationId: "iid",
+      authorEmail: "ae",
+      maxCachedMessageAge: 2,
+      commandLine,
+      userMapboxToken: token,
+    })
+  })
+
   // Install a mock token in our token fetcher so that we don't hit
   // the network.
   beforeEach(() => {
-    MapboxToken["token"] = "mockToken"
+    MapboxToken["token"] = token
+    MapboxToken["commandLine"] = commandLine
   })
 
   afterEach(() => {
     MapboxToken["token"] = undefined
+    MapboxToken["commandLine"] = undefined
   })
 
   it("renders without crashing", async () => {
     const props = getProps()
-    const WrappedComponent = withMapboxToken(TestComponent)
+    const WrappedComponent = withMapboxToken("st.test")(TestComponent)
     const wrapper = shallow(<WrappedComponent {...props} />)
 
     expect(wrapper.html()).not.toBeNull()
@@ -59,7 +78,7 @@ describe("withMapboxToken", () => {
 
   it("passes mapboxToken to wrapped component", async () => {
     const props = getProps()
-    const WrappedComponent = withMapboxToken(TestComponent)
+    const WrappedComponent = withMapboxToken("st.test")(TestComponent)
     const wrapper = shallow(<WrappedComponent {...props} />)
 
     // Wait one tick for our MapboxToken promise to resolve