Skip to content

Commit 8471b2f

Browse files
authored
Merge pull request #36 from NillionNetwork/feat/questionary
Added script for turning any nada program and test into a streamlit app
2 parents 1465877 + 5f4d9ef commit 8471b2f

18 files changed

+284
-134
lines changed

generate-streamlit-app.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import questionary
2+
from pathlib import Path
3+
import os
4+
import yaml
5+
import subprocess
6+
import shutil
7+
import sys
8+
9+
STREAMLIT_APP_TEMPLATE = '''# This file was automatically generated by the generate-streamlit-app script.
10+
# To run this file: from the root directory run `streamlit run streamlit_demo_apps/app_{program_name}.py`
11+
12+
import sys
13+
import os
14+
15+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16+
import streamlit_app
17+
18+
program_name = "{program_name}"
19+
program_test_name = "{program_test_name}"
20+
21+
def main():
22+
current_dir = os.path.dirname(os.path.abspath(__file__))
23+
path_nada_bin = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.bin")
24+
path_nada_json = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.json")
25+
26+
if not os.path.exists(path_nada_bin):
27+
raise FileNotFoundError(f"Add `{{program_name}}.nada.bin` to the compiled_nada_programs folder.")
28+
if not os.path.exists(path_nada_json):
29+
raise FileNotFoundError(f"Run nada build --mir-json and add `{{program_name}}.nada.json` to the compiled_nada_programs folder.")
30+
31+
streamlit_app.main(program_test_name, path_nada_bin, path_nada_json)
32+
33+
if __name__ == "__main__":
34+
main()
35+
'''
36+
37+
def get_programs(directory):
38+
return sorted([f.stem for f in Path(directory).glob('*.py') if f.is_file()])
39+
40+
def get_test_files(directory, program_name):
41+
matching_files = []
42+
for file in Path(directory).glob('*.yaml'):
43+
try:
44+
with open(file, 'r') as f:
45+
test_data = yaml.safe_load(f)
46+
if test_data and 'program' in test_data and test_data['program'] == program_name:
47+
matching_files.append(file)
48+
except yaml.YAMLError:
49+
print(f"Error reading {file}. Skipping.")
50+
except Exception as e:
51+
print(f"Unexpected error reading {file}: {e}. Skipping.")
52+
return matching_files
53+
54+
def select_program_and_test():
55+
programs = get_programs('src')
56+
if not programs:
57+
print("No Python programs found in 'src' directory.")
58+
return None, None
59+
60+
selected_program = questionary.select(
61+
"Select an existing program to create a streamlit app demo:",
62+
choices=programs
63+
).ask()
64+
65+
test_files = get_test_files('tests', selected_program)
66+
if not test_files:
67+
print(f"No test files found for '{selected_program}' in 'tests' directory.")
68+
return selected_program, None
69+
70+
selected_test = questionary.select(
71+
"Select a test file for starting input values:",
72+
choices=[f.name for f in test_files]
73+
).ask()
74+
75+
return selected_program, selected_test
76+
77+
def build_nada_program(program_name):
78+
try:
79+
subprocess.run(['nada', 'build', program_name, '--mir-json'], check=True)
80+
print(f"Successfully built {program_name}")
81+
return True
82+
except subprocess.CalledProcessError as e:
83+
print(f"Error building {program_name}: {e}")
84+
return False
85+
except FileNotFoundError:
86+
print("Error: 'nada' command not found. Make sure it's installed and in your PATH.")
87+
return False
88+
89+
def copy_nada_files(program_name):
90+
source_dir = Path('target')
91+
dest_dir = Path('streamlit_demo_apps/compiled_nada_programs')
92+
93+
for ext in ['.nada.json', '.nada.bin']:
94+
source_file = source_dir / f"{program_name}{ext}"
95+
dest_file = dest_dir / f"{program_name}{ext}"
96+
97+
if source_file.exists():
98+
shutil.copy2(source_file, dest_file)
99+
print(f"Copied {source_file} to {dest_file}")
100+
else:
101+
print(f"Warning: {source_file} not found")
102+
103+
def create_streamlit_app(program_name, test_name):
104+
try:
105+
app_content = STREAMLIT_APP_TEMPLATE.format(
106+
program_name=program_name,
107+
program_test_name=test_name
108+
)
109+
110+
app_file_path = Path('streamlit_demo_apps') / f"app_{program_name}.py"
111+
print(f"Attempting to create file at: {app_file_path.absolute()}")
112+
113+
# Ensure the directory exists
114+
app_file_path.parent.mkdir(parents=True, exist_ok=True)
115+
116+
with open(app_file_path, 'w') as f:
117+
f.write(app_content)
118+
print(f"Created Streamlit app file: {app_file_path}")
119+
120+
if app_file_path.exists():
121+
print(f"Streamlit app file successfully created at {app_file_path}")
122+
return app_file_path
123+
else:
124+
print(f"Error: File creation verified failed for {app_file_path}")
125+
return None
126+
except Exception as e:
127+
print(f"Error creating Streamlit app file: {e}")
128+
return None
129+
130+
def run_streamlit_app(app_path):
131+
try:
132+
print(f"Attempting to run Streamlit app: {app_path}")
133+
subprocess.run([sys.executable, '-m', 'streamlit', 'run', str(app_path)], check=True)
134+
except subprocess.CalledProcessError as e:
135+
print(f"Error running Streamlit app: {e}")
136+
except Exception as e:
137+
print(f"Unexpected error running Streamlit app: {e}")
138+
139+
def main():
140+
program, test = select_program_and_test()
141+
142+
if program:
143+
print(f"Selected program: {program}")
144+
if test:
145+
print(f"Selected test file: {test}")
146+
147+
with open(os.path.join('tests', test), 'r') as file:
148+
test_data = yaml.safe_load(file)
149+
print("\nTest file contents:")
150+
print(yaml.dump(test_data, default_flow_style=False))
151+
152+
if build_nada_program(program):
153+
copy_nada_files(program)
154+
app_path = create_streamlit_app(program, os.path.splitext(test)[0] if test else '')
155+
if app_path:
156+
run_streamlit_app(app_path)
157+
else:
158+
print("No program selected.")
159+
160+
if __name__ == "__main__":
161+
main()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ nada-test
55
# streamlit demo dependencies
66
pyyaml
77
streamlit
8+
questionary
89

910
# nillion_client_script dependencies for streamlit demo
1011
py-nillion-client

streamlit_app.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,39 @@ def parse_nada_json(json_data):
104104

105105
return input_info, output_info
106106

107+
import streamlit as st
108+
109+
def create_party_inputs(input_info, input_values):
110+
party_names = sorted(set(info['party'] for info in input_info.values()))
111+
updated_input_values = input_values.copy()
112+
113+
if len(party_names) > 1:
114+
# Create two columns if there's more than one party
115+
columns = st.columns(2)
116+
else:
117+
# Create a single column if there's only one party
118+
columns = [st.columns(1)[0]]
119+
120+
# Distribute parties between the columns
121+
for i, party_name in enumerate(party_names):
122+
with columns[i % len(columns)]:
123+
st.subheader(f"{party_name}'s Inputs")
124+
for input_name, value in input_values.items():
125+
if input_info[input_name]['party'] == party_name:
126+
input_type = input_info[input_name]['type']
127+
if input_type == 'SecretBoolean':
128+
updated_input_values[input_name] = st.checkbox(
129+
label=f"{input_type}: {input_name}",
130+
value=bool(value)
131+
)
132+
else:
133+
updated_input_values[input_name] = st.number_input(
134+
label=f"{input_type}: {input_name}",
135+
value=value
136+
)
137+
138+
return updated_input_values
139+
107140
def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
108141
# pass test name in via the command line
109142
if nada_test_file_name is None:
@@ -153,35 +186,19 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
153186

154187
# Display the program code
155188
st.subheader(f"{program_name}.py")
156-
st.code(program_code, language='python')
189+
with st.expander(f"Nada Program: {program_name}"):
190+
st.code(program_code, language='python')
157191

158192
# Display inputs grouped by party, alphabetized
159-
updated_input_values = {}
160-
# Get unique party names and sort them alphabetically
161-
party_names = sorted(set(info['party'] for info in input_info.values()))
162-
163-
for party_name in party_names:
164-
st.subheader(f"{party_name}'s Inputs")
165-
for input_name, value in input_values.items():
166-
if input_info[input_name]['party'] == party_name:
167-
input_type = input_info[input_name]['type']
168-
if input_type == 'SecretBoolean':
169-
updated_input_values[input_name] = st.checkbox(
170-
label=f"{input_type}: {input_name}",
171-
value=bool(value)
172-
)
173-
else:
174-
updated_input_values[input_name] = st.number_input(
175-
label=f"{input_type}: {input_name}",
176-
value=value
177-
)
193+
updated_input_values = create_party_inputs(input_info, input_values)
178194

179195
output_parties = list(set(output['party'] for output in output_info.values()))
180196

181197
should_store_inputs = st.checkbox("Store secret inputs before running blind computation", value=False)
182198

183199
# Button to store inputs with a loading screen
184200
if st.button('Run blind computation'):
201+
st.divider()
185202
# Conditional spinner text
186203
if should_store_inputs:
187204
spinner_text = "Storing the Nada program, storing inputs, and running blind computation on the Nillion Network Testnet..."
@@ -203,8 +220,6 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
203220
# Call the async store_inputs_and_run_blind_computation function and wait for it to complete
204221
result_message = asyncio.run(store_inputs_and_run_blind_computation(input_data, program_name, output_parties, nilchain_private_key, path_nada_bin, cluster_id_from_streamlit_config, grpc_endpoint_from_streamlit_config, chain_id_from_streamlit_config, bootnodes, should_store_inputs))
205222

206-
st.divider()
207-
208223
st.subheader("Nada Program Result")
209224

210225
st.text('Output(s)')

streamlit_demo_apps/README.md

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,78 @@
11
# Deploying Streamlit Apps
22

3-
Deployed Streamlit apps live here in the streamlit_demo_apps folder.
3+
Follow the steps to deploy a live Streamlit app for your Nada program. The app will connect to the Nillion Testnet to store your Nada program, store secret inputs (or use computation time secrets), and run blind computation.
44

55
## How to add a new Streamlit App
66

7-
### 0. Create a streamlit secrets file and add your nilchain private key within `.streamlit/secrets.toml`
7+
### 0. Fork this repo
8+
9+
### 1. Create a streamlit secrets file
10+
11+
Run this command to create a `.streamlit/secrets.toml` copied from the example.
812

913
```
1014
cp .streamlit/secrets.toml.example .streamlit/secrets.toml
1115
```
1216

13-
### 1. Create an app file in the streamlit_demo_apps folder
14-
15-
Check out the addition app file example:
17+
Add your Nilchain private key to the .streamlit/secrets.toml file. The private key must be linked to a funded Nillion Testnet address that was created using a Google account (not a mnemonic). This allows you to retrieve the private key from Keplr. If you don’t have a Testnet wallet yet, you can learn how to create one here: https://docs.nillion.com/testnet-guides
1618

17-
`app_addition.py`
19+
### 2. Run the script to generate a new streamlit app for your program
1820

19-
### 2. Copy the compiled Nada program files from the target/ folder into the streamlit_demo_apps/compiled_nada_programs folder
21+
From the root folder of this repo, run the generate-streamlit-app script:
2022

21-
Check out the compiled Nada program files for addition:
22-
23-
nada binary `addition.nada.bin`
24-
nada json `addition.nada.json`
23+
```
24+
python3 generate-streamlit-app.py
25+
```
2526

26-
### 3. Update your app file with the corresponding program name and program test name
27+
### 3. Follow the prompts to
2728

28-
Check out the addition app file example:
29+
- Select an existing program (from the src/ directory)
30+
- Select an existing yaml test file for your program (from the tests/ directory)
2931

30-
`app_addition.py`
32+
This will generate a Streamlit app file: streamlit*demo_apps/app*[your_program_name].py. The script will run the Streamlit app locally with this command
3133

3234
```
33-
program_name = 'addition'
34-
program_test_name = 'addition_test'
35+
streamlit run streamlit_demo_apps/app_[your_program_name].py`
3536
```
3637

3738
### 4. Test your Streamlit app locally
3839

39-
Make sure the apps will work when deployed by testing this command from the root folder.
40+
View the app in your browser to make sure everything works as expected.
41+
42+
### 5. Commit your code to GitHub
43+
44+
Add and commit your new streamlit app code to your forked Github repo. (Code must be connected to a remote, open source GitHub repository to deploy a Streamlit app.)
4045

4146
```
42-
streamlit run streamlit_demo_apps/[app_file_name].py
47+
git add .
48+
git commit -m "my new streamlit nillion app"
49+
git push origin main
4350
```
4451

45-
For example to make sure the addition app will work when deployed, run
52+
Once you've committed the open source code, you can click the "deploy" button within your local streamlit app. Sign in with Github and select the "Deploy Now" on Streamlit Community Cloud option to deploy the app for free.
53+
54+
<img width="1000" alt="Streamlit Community Cloud" src="https://github.com/user-attachments/assets/74a70b4e-506c-41df-8d59-f949871c9a4e">
55+
56+
### 6. Deploy your app from Streamlit.io
57+
58+
When you click "Deploy Now" from your local app, you'll be taken to streamlit.io and asked to log in with Github to create a new Streamlit app. Set the main file path to your new app `streamlit_demo_apps/app_[your_program_name].py`
59+
60+
<img width="1000" alt="streamlit settings" src="https://github.com/user-attachments/assets/e3821aa4-44b6-4f16-8400-97e531dfef23">
61+
62+
#### Add your Nilchain Private Key using Advanced Settings > Secrets
63+
64+
Go to "Advanced settings" and in Secrets, copy in the contents of your .streamlit/secrets.toml file. At a minimum, make sure to add your secret private key:
4665

4766
```
48-
streamlit run streamlit_demo_apps/app_addition.py
67+
nilchain_private_key = "YOUR_FUNDED_PRIVATE_KEY"
4968
```
69+
70+
<img width="1000" alt="advanced settings" src="https://github.com/user-attachments/assets/6b48b225-60b7-41bd-8591-c04419131bf8">
71+
72+
Save and click "Deploy" to deploy your testnet-connected Streamlit app.
73+
74+
### 7. Access Your Live Streamlit App
75+
76+
Once deployed, you’ll get a live link to your Nillion Testnet Streamlit app!
77+
78+
Example live Streamlit App: https://stephs-nada-multiplication-app.streamlit.app/

streamlit_demo_apps/app_addition.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1+
# This file was automatically generated by the generate-streamlit-app script.
2+
# To run this file: from the root directory run `streamlit run streamlit_demo_apps/app_addition.py`
3+
14
import sys
25
import os
36

47
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
5-
import streamlit_app
8+
import streamlit_app
69

7-
program_name = 'addition'
8-
program_test_name = 'addition_test'
10+
program_name = "addition"
11+
program_test_name = "addition_test"
912

1013
def main():
1114
current_dir = os.path.dirname(os.path.abspath(__file__))
1215
path_nada_bin = os.path.join(current_dir, "compiled_nada_programs", f"{program_name}.nada.bin")
1316
path_nada_json = os.path.join(current_dir, "compiled_nada_programs", f"{program_name}.nada.json")
17+
1418
if not os.path.exists(path_nada_bin):
1519
raise FileNotFoundError(f"Add `{program_name}.nada.bin` to the compiled_nada_programs folder.")
1620
if not os.path.exists(path_nada_json):
1721
raise FileNotFoundError(f"Run nada build --mir-json and add `{program_name}.nada.json` to the compiled_nada_programs folder.")
22+
1823
streamlit_app.main(program_test_name, path_nada_bin, path_nada_json)
1924

2025
if __name__ == "__main__":
2126
main()
22-

0 commit comments

Comments
 (0)