marcenacp commited on
Commit
bbea1cc
1 Parent(s): cb5b71d

Add authentication

Browse files
Files changed (6) hide show
  1. app.py +29 -3
  2. core/constants.py +22 -2
  3. core/past_projects.py +5 -2
  4. core/state.py +53 -1
  5. deploy_to_hf.sh +14 -0
  6. views/splash.py +7 -0
app.py CHANGED
@@ -1,6 +1,12 @@
 
 
1
  import streamlit as st
2
 
 
 
 
3
  from core.state import CurrentStep
 
4
  from utils import init_state
5
  from views.splash import render_splash
6
  from views.wizard import render_editor
@@ -8,14 +14,34 @@ from views.wizard import render_editor
8
  init_state()
9
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def _back_to_menu():
12
  """Sends the user back to the menu."""
13
  init_state(force=True)
14
 
15
 
16
- st.set_page_config(page_title="Croissant Editor", page_icon="🥐", layout="wide")
17
- col1, col2 = st.columns([10, 1])
18
- col1.header("Croissant Editor")
19
  if st.session_state[CurrentStep] != CurrentStep.splash:
20
  col2.write("\n") # Vertical box to shift the button menu
21
  col2.button("Menu", on_click=_back_to_menu)
 
1
+ import urllib.parse
2
+
3
  import streamlit as st
4
 
5
+ from core.constants import OAUTH_CLIENT_ID
6
+ from core.constants import OAUTH_STATE
7
+ from core.constants import REDIRECT_URI
8
  from core.state import CurrentStep
9
+ from core.state import User
10
  from utils import init_state
11
  from views.splash import render_splash
12
  from views.wizard import render_editor
 
14
  init_state()
15
 
16
 
17
+ st.set_page_config(page_title="Croissant Editor", page_icon="🥐", layout="wide")
18
+ col1, col2 = st.columns([10, 1])
19
+ col1.header("Croissant Editor")
20
+
21
+ if OAUTH_CLIENT_ID and not st.session_state.get(User):
22
+ query_params = st.experimental_get_query_params()
23
+ state = query_params.get("state")
24
+ if state and state[0] == OAUTH_STATE:
25
+ code = query_params.get("code")
26
+ if not code:
27
+ st.stop()
28
+ st.session_state[User] = User.connect(code)
29
+ st.experimental_set_query_params()
30
+ else:
31
+ redirect_uri = urllib.parse.quote(REDIRECT_URI, safe="")
32
+ client_id = urllib.parse.quote(OAUTH_CLIENT_ID, safe="")
33
+ state = urllib.parse.quote(OAUTH_STATE, safe="")
34
+ scope = urllib.parse.quote("openid profile", safe="")
35
+ url = f"https://huggingface.co/oauth/authorize?response_type=code&redirect_uri={redirect_uri}&scope={scope}&client_id={client_id}&state={state}"
36
+ st.link_button("🤗 Login with Hugging Face", url)
37
+ st.stop()
38
+
39
+
40
  def _back_to_menu():
41
  """Sends the user back to the menu."""
42
  init_state(force=True)
43
 
44
 
 
 
 
45
  if st.session_state[CurrentStep] != CurrentStep.splash:
46
  col2.write("\n") # Vertical box to shift the button menu
47
  col2.button("Menu", on_click=_back_to_menu)
core/constants.py CHANGED
@@ -1,10 +1,30 @@
 
 
1
  from etils import epath
2
 
3
  import mlcroissant as mlc
4
 
 
 
 
 
 
 
5
  EDITOR_CACHE: epath.Path = mlc.constants.CROISSANT_CACHE / "editor"
6
- PAST_PROJECTS_PATH: epath.Path = EDITOR_CACHE / "projects"
7
- PROJECT_FOLDER_PATTERN = "%Y%m%d%H%M%S%f"
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  DF_HEIGHT = 150
 
1
+ import os
2
+
3
  from etils import epath
4
 
5
  import mlcroissant as mlc
6
 
7
+ # Authentication to Hugging Face:
8
+ REDIRECT_URI = os.getenv("REDIRECT_URI", "http://localhost:8501")
9
+ OAUTH_STATE = os.getenv("OAUTH_STATE")
10
+ OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID")
11
+ OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET")
12
+
13
  EDITOR_CACHE: epath.Path = mlc.constants.CROISSANT_CACHE / "editor"
 
 
14
 
15
 
16
+ def PAST_PROJECTS_PATH(user) -> epath.Path:
17
+ base = EDITOR_CACHE / "projects"
18
+ # If there is authentication, look up in the user's path:
19
+ if OAUTH_CLIENT_ID:
20
+ if user is None:
21
+ raise Exception("Please, authenticate before using the application.")
22
+ return base / user.username
23
+ # Else look up at the root:
24
+ else:
25
+ return base
26
+
27
+
28
+ PROJECT_FOLDER_PATTERN = "%Y%m%d%H%M%S%f"
29
+
30
  DF_HEIGHT = 150
core/past_projects.py CHANGED
@@ -7,11 +7,14 @@ import streamlit as st
7
  from core.constants import PAST_PROJECTS_PATH
8
  from core.state import CurrentProject
9
  from core.state import Metadata
 
10
 
11
 
12
  def load_past_projects_paths() -> list[epath.Path]:
13
- PAST_PROJECTS_PATH.mkdir(parents=True, exist_ok=True)
14
- return sorted(list(PAST_PROJECTS_PATH.iterdir()), reverse=True)
 
 
15
 
16
 
17
  def _pickle_file(path: epath.Path) -> epath.Path:
 
7
  from core.constants import PAST_PROJECTS_PATH
8
  from core.state import CurrentProject
9
  from core.state import Metadata
10
+ from core.state import User
11
 
12
 
13
  def load_past_projects_paths() -> list[epath.Path]:
14
+ user = st.session_state.get(User)
15
+ past_projects_path = PAST_PROJECTS_PATH(user)
16
+ past_projects_path.mkdir(parents=True, exist_ok=True)
17
+ return sorted(list(past_projects_path.iterdir()), reverse=True)
18
 
19
 
20
  def _pickle_file(path: epath.Path) -> epath.Path:
core/state.py CHANGED
@@ -5,15 +5,22 @@ In the future, this could be the serialization format between front and back.
5
 
6
  from __future__ import annotations
7
 
 
8
  import dataclasses
9
  import datetime
 
10
  from typing import Any
11
 
12
  from etils import epath
13
  import pandas as pd
 
 
14
 
 
 
15
  from core.constants import PAST_PROJECTS_PATH
16
  from core.constants import PROJECT_FOLDER_PATTERN
 
17
  import mlcroissant as mlc
18
 
19
 
@@ -28,6 +35,46 @@ def create_class(mlc_class: type, instance: Any, **kwargs) -> Any:
28
  return mlc_class(**params, **kwargs)
29
 
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  class CurrentStep:
32
  """Holds all major state variables for the application."""
33
 
@@ -44,7 +91,12 @@ class CurrentProject:
44
  @classmethod
45
  def create_new(cls) -> CurrentProject:
46
  timestamp = datetime.datetime.now().strftime(PROJECT_FOLDER_PATTERN)
47
- return CurrentProject(path=PAST_PROJECTS_PATH / timestamp)
 
 
 
 
 
48
 
49
 
50
  class SelectedResource:
 
5
 
6
  from __future__ import annotations
7
 
8
+ import base64
9
  import dataclasses
10
  import datetime
11
+ import hashlib
12
  from typing import Any
13
 
14
  from etils import epath
15
  import pandas as pd
16
+ import requests
17
+ import streamlit as st
18
 
19
+ from core.constants import OAUTH_CLIENT_ID
20
+ from core.constants import OAUTH_CLIENT_SECRET
21
  from core.constants import PAST_PROJECTS_PATH
22
  from core.constants import PROJECT_FOLDER_PATTERN
23
+ from core.constants import REDIRECT_URI
24
  import mlcroissant as mlc
25
 
26
 
 
35
  return mlc_class(**params, **kwargs)
36
 
37
 
38
+ @dataclasses.dataclass
39
+ class User:
40
+ """The connected user."""
41
+
42
+ access_token: str
43
+ id_token: str
44
+ username: str
45
+
46
+ @classmethod
47
+ def connect(cls, code: str):
48
+ credentials = base64.b64encode(
49
+ f"{OAUTH_CLIENT_ID}:{OAUTH_CLIENT_SECRET}".encode()
50
+ ).decode()
51
+ headers = {
52
+ "Authorization": f"Basic {credentials}",
53
+ }
54
+ data = {
55
+ "client_id": OAUTH_CLIENT_ID,
56
+ "grant_type": "authorization_code",
57
+ "code": code,
58
+ "redirect_uri": REDIRECT_URI,
59
+ }
60
+ url = "https://huggingface.co/oauth/token"
61
+ response = requests.post(url, data=data, headers=headers)
62
+ if response.status_code == 200:
63
+ response = response.json()
64
+ access_token = response.get("access_token")
65
+ id_token = response.get("id_token")
66
+ if access_token and id_token:
67
+ # Warning: this is temporary while being able to retrieve the username.
68
+ username = hashlib.sha256(access_token.encode()).hexdigest()
69
+ return User(
70
+ access_token=access_token, username=username, id_token=id_token
71
+ )
72
+ raise Exception(
73
+ f"Could not connect to Hugging Face. Please, go to {REDIRECT_URI}."
74
+ f" ({response=})."
75
+ )
76
+
77
+
78
  class CurrentStep:
79
  """Holds all major state variables for the application."""
80
 
 
91
  @classmethod
92
  def create_new(cls) -> CurrentProject:
93
  timestamp = datetime.datetime.now().strftime(PROJECT_FOLDER_PATTERN)
94
+ user = st.session_state.get(User)
95
+ if user is None:
96
+ return None
97
+ else:
98
+ path = PAST_PROJECTS_PATH(user)
99
+ return CurrentProject(path=path / timestamp)
100
 
101
 
102
  class SelectedResource:
deploy_to_hf.sh ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ HF_REPO=/tmp/hf-croissant
2
+ echo "Deleting $HF_REPO..."
3
+ rm -rf ${HF_REPO}
4
+ git clone [email protected]:spaces/marcenacp/croissant-editor ${HF_REPO}
5
+ echo "Copying files from $PWD to $HF_REPO..."
6
+ rsync -aP --exclude="README.md" --exclude="*node_modules*" --exclude="*__pycache__*" . ${HF_REPO}
7
+ cd ${HF_REPO}
8
+ echo "Now push with: 'cd $HF_REPO && git add && git commit && git push'."
9
+ echo "Warning: if it fails, you may need to follow https://huggingface.co/docs/hub/security-git-ssh#generating-a-new-ssh-keypair"
10
+ echo "On Hugging Face Spaces, you might have to set the following environment variables:"
11
+ echo "- REDIRECT_URI"
12
+ echo "- OAUTH_STATE"
13
+ echo "- OAUTH_CLIENT_ID"
14
+ echo "- OAUTH_CLIENT_SECRET"
views/splash.py CHANGED
@@ -1,5 +1,6 @@
1
  import streamlit as st
2
 
 
3
  from core.state import CurrentProject
4
  from core.state import CurrentStep
5
  from core.state import Metadata
@@ -10,6 +11,12 @@ from views.side_buttons import jump_to
10
 
11
 
12
  def render_splash():
 
 
 
 
 
 
13
  col1, col2 = st.columns([1, 1], gap="large")
14
  with col1:
15
  with st.expander("**Load an existing Croissant JSON-LD file**", expanded=True):
 
1
  import streamlit as st
2
 
3
+ from core.constants import OAUTH_CLIENT_ID
4
  from core.state import CurrentProject
5
  from core.state import CurrentStep
6
  from core.state import Metadata
 
11
 
12
 
13
  def render_splash():
14
+ if OAUTH_CLIENT_ID:
15
+ st.write(
16
+ "**Disclaimer**: Do not put sensitive information or datasets here. If you"
17
+ " want to host your own version locally, build the app from [the GitHub"
18
+ " repository](https://github.com/mlcommons/croissant/tree/main/editor)."
19
+ )
20
  col1, col2 = st.columns([1, 1], gap="large")
21
  with col1:
22
  with st.expander("**Load an existing Croissant JSON-LD file**", expanded=True):