thearn commited on
Commit
d7e687c
ยท
1 Parent(s): 412abad
Files changed (3) hide show
  1. README.md +30 -16
  2. app.py +114 -15
  3. src/app.py +0 -119
README.md CHANGED
@@ -1,25 +1,39 @@
1
- # math_roots
 
 
 
 
 
 
 
2
 
3
- A Python project to visualize mathematician ancestor trees from the Math Genealogy Project using [Geneagrapher](https://github.com/davidalber/geneagrapher) and Streamlit.
4
 
5
- ## Features
6
 
7
- - Look up a mathematician by Math Genealogy Project user ID
8
- - Fetch and display their academic ancestor tree as a directed graph
9
 
10
- ## Setup
 
11
 
12
- ```sh
13
- poetry install
14
- ```
15
 
16
- ## Running Tests
 
 
 
 
 
 
 
 
17
 
18
- ```sh
19
- poetry run pytest
20
- ```
21
 
22
- ## Running the App
 
 
23
 
24
- ```sh
25
- poetry run streamlit run src/app.py
 
 
1
+ ---
2
+ library_name: streamlit
3
+ tags:
4
+ - streamlit
5
+ - math-genealogy
6
+ - webapp
7
+ app_file: app.py
8
+ ---
9
 
10
+ # ๐Ÿ“š Math Roots: Math Genealogy Tree Explorer
11
 
12
+ A simple Streamlit app to explore your mathematical genealogy tree using your Math Genealogy Project ID.
13
 
14
+ ## ๐ŸŒŸ Features
 
15
 
16
+ - ๐Ÿง‘โ€๐ŸŽ“ Enter your Math Genealogy Project ID to trace your academic "ancestors"
17
+ - ๐ŸŒณ Interactive, web-based interface powered by [Streamlit](https://streamlit.io/)
18
 
19
+ ## ๐Ÿš€ Usage
 
 
20
 
21
+ 1. Install dependencies:
22
+ ```bash
23
+ pip install streamlit
24
+ ```
25
+ 2. Run the app:
26
+ ```bash
27
+ streamlit run app.py
28
+ ```
29
+ 3. Open the provided local URL in your browser.
30
 
31
+ ## ๐Ÿ“ Model Card
 
 
32
 
33
+ **Library:** Streamlit
34
+ **App Entry Point:** `app.py`
35
+ **Tags:** math-genealogy, streamlit, webapp
36
 
37
+ ## ๐Ÿ“„ License
38
+
39
+ MIT
app.py CHANGED
@@ -1,20 +1,119 @@
1
  import streamlit as st
 
 
 
 
 
2
 
3
- st.title("Math Genealogy Ancestor Tree")
4
 
5
- user_id = st.text_input("Enter Math Genealogy Project user ID:")
 
 
 
6
 
7
- if user_id:
8
- # placeholder DOT graph, replace with real output later
9
- dot = '''
10
- digraph G {
11
- A [label="User "]
12
- B [label="Advisor 1"]
13
- C [label="Advisor 2"]
14
- A -> B
15
- B -> C
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
- '''
18
- st.graphviz_chart(dot)
19
- else:
20
- st.info("Enter a user ID to view the ancestor tree.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import asyncio
3
+ import websockets
4
+ import json
5
+ import platform
6
+ from typing import Dict, Any, Optional, List, Literal, TypedDict, cast
7
 
8
+ GGRAPHER_URI = "wss://ggrphr.davidalber.net"
9
 
10
+ class StartNodeRequest(TypedDict):
11
+ recordId: int
12
+ getAdvisors: bool
13
+ getDescendants: bool
14
 
15
+ class RequestPayload(TypedDict):
16
+ kind: Literal["build-graph"]
17
+ options: Dict[Literal["reportingCallback"], bool]
18
+ startNodes: List[StartNodeRequest]
19
+
20
+ class ProgressCallback(TypedDict):
21
+ queued: int
22
+ fetching: int
23
+ done: int
24
+
25
+ def get_id_from_input(val: str) -> Optional[int]:
26
+ try:
27
+ return int(val)
28
+ except Exception:
29
+ return None
30
+
31
+ def make_payload(record_id: int) -> RequestPayload:
32
+ return {
33
+ "kind": "build-graph",
34
+ "options": {"reportingCallback": True},
35
+ "startNodes": [{
36
+ "recordId": record_id,
37
+ "getAdvisors": True,
38
+ "getDescendants": False,
39
+ }],
40
  }
41
+
42
+ async def get_graph(payload: RequestPayload, progress_cb=None) -> Dict[str, Any]:
43
+ def intify_record_keys(d: Dict[Any, Any]) -> Dict[Any, Any]:
44
+ if "nodes" in d:
45
+ ret = {k: v for k, v in d.items() if k != "nodes"}
46
+ ret["nodes"] = {int(k): v for k, v in d["nodes"].items()}
47
+ return ret
48
+ return d
49
+
50
+ async with websockets.connect( # type: ignore[attr-defined]
51
+ GGRAPHER_URI,
52
+ ) as ws:
53
+ await ws.send(json.dumps(payload))
54
+ while True:
55
+ response_json = await ws.recv()
56
+ response = json.loads(response_json, object_hook=intify_record_keys)
57
+ response_payload = response.get("payload")
58
+ if response["kind"] == "graph":
59
+ return cast(Dict[str, Any], response_payload)
60
+ elif response["kind"] == "progress" and progress_cb:
61
+ progress = cast(ProgressCallback, response_payload)
62
+ progress_cb(progress)
63
+ else:
64
+ continue
65
+
66
+ def tree_to_dot(graph: Dict[str, Any]) -> str:
67
+ nodes = graph.get("nodes", {})
68
+ lines = [
69
+ "digraph G {",
70
+ " rankdir=TB;",
71
+ ' node [shape=box, style="rounded,filled", fillcolor=lightyellow];',
72
+ ' edge [arrowhead=vee];'
73
+ ]
74
+ # Define nodes and their labels
75
+ for node_id, node in nodes.items():
76
+ name = node.get("name", str(node_id))
77
+ year_str = f" ({node.get('year')})" if node.get('year') is not None else " (Year Unknown)"
78
+ label = f"{name}{year_str}"
79
+ tooltip = f"ID: {node_id}\\nName: {name}\\nYear: {node.get('year', 'N/A')}\\nInstitution: {node.get('institution', 'N/A')}"
80
+ lines.append(f' "{node_id}" [label="{label}", tooltip="{tooltip}"];')
81
+ # Define edges
82
+ for node_id, node in nodes.items():
83
+ for adv_id in node.get("advisors", []):
84
+ if adv_id in nodes:
85
+ lines.append(f' "{adv_id}" -> "{node_id}";')
86
+ lines.append("}")
87
+ return "\n".join(lines)
88
+
89
+ def main():
90
+ st.title("Math Genealogy Ancestor Tree (WebSocket API)")
91
+ mgp_id_str = st.text_input("Enter MGP ID (integer):")
92
+ progress_placeholder = st.empty()
93
+ graph_placeholder = st.empty()
94
+ run_btn = st.button("Show Ancestor Tree")
95
+ if run_btn:
96
+ mgp_id = get_id_from_input(mgp_id_str)
97
+ if mgp_id is None:
98
+ st.error("Please enter a valid integer MGP ID.")
99
+ return
100
+ payload = make_payload(mgp_id)
101
+ loop = asyncio.new_event_loop()
102
+ asyncio.set_event_loop(loop)
103
+ def progress_cb(progress):
104
+ progress_placeholder.info(
105
+ f"Queued: {progress['queued']} | Fetching: {progress['fetching']} | Done: {progress['done']}"
106
+ )
107
+ async def runner():
108
+ graph = await get_graph(payload, progress_cb)
109
+ dot = tree_to_dot(graph)
110
+ graph_placeholder.graphviz_chart(dot)
111
+ try:
112
+ loop.run_until_complete(runner())
113
+ progress_placeholder.success("Done!")
114
+ except Exception as e:
115
+ print(f"Error: {e}")
116
+ progress_placeholder.error(f"Error: {e}")
117
+
118
+ if __name__ == "__main__":
119
+ main()
src/app.py DELETED
@@ -1,119 +0,0 @@
1
- import streamlit as st
2
- import asyncio
3
- import websockets
4
- import json
5
- import platform
6
- from typing import Dict, Any, Optional, List, Literal, TypedDict, cast
7
-
8
- GGRAPHER_URI = "wss://ggrphr.davidalber.net"
9
-
10
- class StartNodeRequest(TypedDict):
11
- recordId: int
12
- getAdvisors: bool
13
- getDescendants: bool
14
-
15
- class RequestPayload(TypedDict):
16
- kind: Literal["build-graph"]
17
- options: Dict[Literal["reportingCallback"], bool]
18
- startNodes: List[StartNodeRequest]
19
-
20
- class ProgressCallback(TypedDict):
21
- queued: int
22
- fetching: int
23
- done: int
24
-
25
- def get_id_from_input(val: str) -> Optional[int]:
26
- try:
27
- return int(val)
28
- except Exception:
29
- return None
30
-
31
- def make_payload(record_id: int) -> RequestPayload:
32
- return {
33
- "kind": "build-graph",
34
- "options": {"reportingCallback": True},
35
- "startNodes": [{
36
- "recordId": record_id,
37
- "getAdvisors": True,
38
- "getDescendants": False,
39
- }],
40
- }
41
-
42
- async def get_graph(payload: RequestPayload, progress_cb=None) -> Dict[str, Any]:
43
- def intify_record_keys(d: Dict[Any, Any]) -> Dict[Any, Any]:
44
- if "nodes" in d:
45
- ret = {k: v for k, v in d.items() if k != "nodes"}
46
- ret["nodes"] = {int(k): v for k, v in d["nodes"].items()}
47
- return ret
48
- return d
49
-
50
- async with websockets.connect( # type: ignore[attr-defined]
51
- GGRAPHER_URI,
52
- ) as ws:
53
- await ws.send(json.dumps(payload))
54
- while True:
55
- response_json = await ws.recv()
56
- response = json.loads(response_json, object_hook=intify_record_keys)
57
- response_payload = response.get("payload")
58
- if response["kind"] == "graph":
59
- return cast(Dict[str, Any], response_payload)
60
- elif response["kind"] == "progress" and progress_cb:
61
- progress = cast(ProgressCallback, response_payload)
62
- progress_cb(progress)
63
- else:
64
- continue
65
-
66
- def tree_to_dot(graph: Dict[str, Any]) -> str:
67
- nodes = graph.get("nodes", {})
68
- lines = [
69
- "digraph G {",
70
- " rankdir=TB;",
71
- ' node [shape=box, style="rounded,filled", fillcolor=lightyellow];',
72
- ' edge [arrowhead=vee];'
73
- ]
74
- # Define nodes and their labels
75
- for node_id, node in nodes.items():
76
- name = node.get("name", str(node_id))
77
- year_str = f" ({node.get('year')})" if node.get('year') is not None else " (Year Unknown)"
78
- label = f"{name}{year_str}"
79
- tooltip = f"ID: {node_id}\\nName: {name}\\nYear: {node.get('year', 'N/A')}\\nInstitution: {node.get('institution', 'N/A')}"
80
- lines.append(f' "{node_id}" [label="{label}", tooltip="{tooltip}"];')
81
- # Define edges
82
- for node_id, node in nodes.items():
83
- for adv_id in node.get("advisors", []):
84
- if adv_id in nodes:
85
- lines.append(f' "{adv_id}" -> "{node_id}";')
86
- lines.append("}")
87
- return "\n".join(lines)
88
-
89
- def main():
90
- st.title("Math Genealogy Ancestor Tree (WebSocket API)")
91
- mgp_id_str = st.text_input("Enter MGP ID (integer):")
92
- progress_placeholder = st.empty()
93
- graph_placeholder = st.empty()
94
- run_btn = st.button("Show Ancestor Tree")
95
- if run_btn:
96
- mgp_id = get_id_from_input(mgp_id_str)
97
- if mgp_id is None:
98
- st.error("Please enter a valid integer MGP ID.")
99
- return
100
- payload = make_payload(mgp_id)
101
- loop = asyncio.new_event_loop()
102
- asyncio.set_event_loop(loop)
103
- def progress_cb(progress):
104
- progress_placeholder.info(
105
- f"Queued: {progress['queued']} | Fetching: {progress['fetching']} | Done: {progress['done']}"
106
- )
107
- async def runner():
108
- graph = await get_graph(payload, progress_cb)
109
- dot = tree_to_dot(graph)
110
- graph_placeholder.graphviz_chart(dot)
111
- try:
112
- loop.run_until_complete(runner())
113
- progress_placeholder.success("Done!")
114
- except Exception as e:
115
- print(f"Error: {e}")
116
- progress_placeholder.error(f"Error: {e}")
117
-
118
- if __name__ == "__main__":
119
- main()