Spaces:
Running
Running
Add authentication
Browse files- app.py +29 -3
- core/constants.py +22 -2
- core/past_projects.py +5 -2
- core/state.py +53 -1
- deploy_to_hf.sh +14 -0
- 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 |
-
|
14 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
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):
|