fustor-demo 0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fustor_demo-0.2/PKG-INFO +11 -0
- fustor_demo-0.2/pyproject.toml +24 -0
- fustor_demo-0.2/setup.cfg +4 -0
- fustor_demo-0.2/src/fustor_demo/auto_generator.py +128 -0
- fustor_demo-0.2/src/fustor_demo/main.py +114 -0
- fustor_demo-0.2/src/fustor_demo/mock_agents.py +128 -0
- fustor_demo-0.2/src/fustor_demo/static/index.html +386 -0
- fustor_demo-0.2/src/fustor_demo/store.py +100 -0
- fustor_demo-0.2/src/fustor_demo.egg-info/PKG-INFO +11 -0
- fustor_demo-0.2/src/fustor_demo.egg-info/SOURCES.txt +11 -0
- fustor_demo-0.2/src/fustor_demo.egg-info/dependency_links.txt +1 -0
- fustor_demo-0.2/src/fustor_demo.egg-info/requires.txt +6 -0
- fustor_demo-0.2/src/fustor_demo.egg-info/top_level.txt +1 -0
fustor_demo-0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-demo
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: Fustor Bio-Fusion Demo for unified directory service
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: fastapi[all]>=0.104.1
|
|
7
|
+
Requires-Dist: uvicorn>=0.23.2
|
|
8
|
+
Requires-Dist: fustor-core
|
|
9
|
+
Requires-Dist: fustor-event-model
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: httpx>=0.25.0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fustor-demo"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Fustor Bio-Fusion Demo for unified directory service"
|
|
5
|
+
dependencies = [ "fastapi[all]>=0.104.1", "uvicorn>=0.23.2", "fustor-core", "fustor-event-model", "pydantic>=2.0.0", "httpx>=0.25.0",]
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
|
|
8
|
+
[build-system]
|
|
9
|
+
requires = [ "setuptools>=61.0", "setuptools-scm>=8.0"]
|
|
10
|
+
build-backend = "setuptools.build_meta"
|
|
11
|
+
|
|
12
|
+
[tool.setuptools_scm]
|
|
13
|
+
root = "../.."
|
|
14
|
+
version_scheme = "post-release"
|
|
15
|
+
local_scheme = "dirty-tag"
|
|
16
|
+
|
|
17
|
+
["project.urls"]
|
|
18
|
+
Homepage = "https://github.com/excelwang/fustor/tree/master/packages/demo_bio_fusion"
|
|
19
|
+
"Bug Tracker" = "https://github.com/excelwang/fustor/issues"
|
|
20
|
+
|
|
21
|
+
license = "MIT"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = [ "src",]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List
|
|
5
|
+
from .mock_agents import (
|
|
6
|
+
mock_mysql_create_project,
|
|
7
|
+
mock_nfs_hot_add_file,
|
|
8
|
+
mock_nfs_cold_add_file,
|
|
9
|
+
mock_oss_add_dataset_link,
|
|
10
|
+
mock_es_add_publication
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("fustor_demo.generator")
|
|
14
|
+
|
|
15
|
+
# Sample data pools for random generation
|
|
16
|
+
PROJECT_NAMES = [
|
|
17
|
+
"Cancer_Genomics_2024", "Viral_Study_Beta", "Plant_Diversity_X",
|
|
18
|
+
"Rare_Disease_Gamma", "Microbiome_Gut_Health", "Neuro_Science_Alpha"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
FILE_PREFIXES = ["seq_run", "analysis_result", "raw_image", "patient_data", "gene_expression"]
|
|
22
|
+
FILE_EXTENSIONS = [".fastq", ".bam", ".vcf", ".tiff", ".csv", ".json"]
|
|
23
|
+
|
|
24
|
+
DATASET_NAMES = ["1000_Genomes", "TCGA_Public", "GSA_Reference", "OMIX_Baseline", "UniProt_Dump"]
|
|
25
|
+
|
|
26
|
+
TITLES = [
|
|
27
|
+
"Novel findings in gene X", "Study of protein Y interaction",
|
|
28
|
+
"Large scale population analysis", "Review of viral vectors", "Methodology for fast sequencing"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
class AutoDataGenerator:
|
|
32
|
+
def __init__(self, interval_min: float = 2.0, interval_max: float = 5.0):
|
|
33
|
+
self.interval_min = interval_min
|
|
34
|
+
self.interval_max = interval_max
|
|
35
|
+
self._running = False
|
|
36
|
+
self._task = None
|
|
37
|
+
self._generated_projects: List[str] = [] # Keep track of created projects to add files to them
|
|
38
|
+
|
|
39
|
+
async def start(self):
|
|
40
|
+
if self._running:
|
|
41
|
+
return
|
|
42
|
+
self._running = True
|
|
43
|
+
self._task = asyncio.create_task(self._loop())
|
|
44
|
+
logger.info("Auto data generator started.")
|
|
45
|
+
|
|
46
|
+
async def stop(self):
|
|
47
|
+
if not self._running:
|
|
48
|
+
return
|
|
49
|
+
self._running = False
|
|
50
|
+
if self._task:
|
|
51
|
+
self._task.cancel()
|
|
52
|
+
try:
|
|
53
|
+
await self._task
|
|
54
|
+
except asyncio.CancelledError:
|
|
55
|
+
pass
|
|
56
|
+
logger.info("Auto data generator stopped.")
|
|
57
|
+
|
|
58
|
+
async def _loop(self):
|
|
59
|
+
# Initial seeding: Create a couple of projects
|
|
60
|
+
for name in PROJECT_NAMES[:2]:
|
|
61
|
+
self._create_project(name)
|
|
62
|
+
await asyncio.sleep(0.5)
|
|
63
|
+
|
|
64
|
+
while self._running:
|
|
65
|
+
try:
|
|
66
|
+
# Pick a random action
|
|
67
|
+
action_type = random.choice([
|
|
68
|
+
"create_project",
|
|
69
|
+
"add_nfs_hot", "add_nfs_hot", "add_nfs_hot", # Higher weight for files
|
|
70
|
+
"add_nfs_cold",
|
|
71
|
+
"add_oss",
|
|
72
|
+
"add_es"
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
if action_type == "create_project":
|
|
76
|
+
# Create a new project from the list if not all exist, or a random variant
|
|
77
|
+
available_names = [p for p in PROJECT_NAMES if p.lower() not in self._generated_projects]
|
|
78
|
+
if available_names:
|
|
79
|
+
self._create_project(random.choice(available_names))
|
|
80
|
+
else:
|
|
81
|
+
# Create a variant
|
|
82
|
+
base = random.choice(PROJECT_NAMES)
|
|
83
|
+
variant = f"{base}_v{random.randint(1, 99)}"
|
|
84
|
+
self._create_project(variant)
|
|
85
|
+
|
|
86
|
+
elif self._generated_projects: # Only add items if we have projects
|
|
87
|
+
project_id = random.choice(self._generated_projects)
|
|
88
|
+
|
|
89
|
+
if action_type == "add_nfs_hot":
|
|
90
|
+
fname = f"{random.choice(FILE_PREFIXES)}_{random.randint(100, 999)}{random.choice(FILE_EXTENSIONS)}"
|
|
91
|
+
size = random.randint(10, 5000)
|
|
92
|
+
mock_nfs_hot_add_file(project_id, fname, size)
|
|
93
|
+
logger.info(f"Auto-generated NFS Hot file '{fname}' for '{project_id}'")
|
|
94
|
+
|
|
95
|
+
elif action_type == "add_nfs_cold":
|
|
96
|
+
fname = f"archive_{random.choice(FILE_PREFIXES)}_{random.randint(2010, 2023)}{random.choice(FILE_EXTENSIONS)}"
|
|
97
|
+
size = random.randint(50, 200) # GB
|
|
98
|
+
mock_nfs_cold_add_file(project_id, fname, size)
|
|
99
|
+
logger.info(f"Auto-generated NFS Cold file '{fname}' for '{project_id}'")
|
|
100
|
+
|
|
101
|
+
elif action_type == "add_oss":
|
|
102
|
+
ds_name = f"{random.choice(DATASET_NAMES)}_{random.randint(1, 10)}.tar.gz"
|
|
103
|
+
mock_oss_add_dataset_link(project_id, ds_name, f"https://oss.example.com/{ds_name}")
|
|
104
|
+
logger.info(f"Auto-generated OSS link '{ds_name}' for '{project_id}'")
|
|
105
|
+
|
|
106
|
+
elif action_type == "add_es":
|
|
107
|
+
title = f"{random.choice(TITLES)} - {random.randint(1, 100)}"
|
|
108
|
+
pid = str(random.randint(10000000, 99999999))
|
|
109
|
+
mock_es_add_publication(project_id, title, pid)
|
|
110
|
+
logger.info(f"Auto-generated ES citation '{title}' for '{project_id}'")
|
|
111
|
+
|
|
112
|
+
# Sleep for random interval
|
|
113
|
+
delay = random.uniform(self.interval_min, self.interval_max)
|
|
114
|
+
await asyncio.sleep(delay)
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Error in auto generator loop: {e}")
|
|
118
|
+
await asyncio.sleep(5) # Retry delay
|
|
119
|
+
|
|
120
|
+
def _create_project(self, name: str):
|
|
121
|
+
mock_mysql_create_project(name)
|
|
122
|
+
project_id = name.lower().replace(" ", "_")
|
|
123
|
+
if project_id not in self._generated_projects:
|
|
124
|
+
self._generated_projects.append(project_id)
|
|
125
|
+
logger.info(f"Auto-generated Project '{name}'")
|
|
126
|
+
|
|
127
|
+
# Global instance
|
|
128
|
+
generator = AutoDataGenerator()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from fastapi import FastAPI, Request, HTTPException, status
|
|
2
|
+
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
|
|
3
|
+
from fastapi.staticfiles import StaticFiles
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
from fustor_event_model.models import EventBase, EventType # For type hinting if needed
|
|
10
|
+
|
|
11
|
+
from fustor_demo.store import demo_store
|
|
12
|
+
from fustor_demo.mock_agents import (
|
|
13
|
+
mock_mysql_create_project,
|
|
14
|
+
mock_nfs_hot_add_file,
|
|
15
|
+
mock_nfs_cold_add_file,
|
|
16
|
+
mock_oss_add_dataset_link,
|
|
17
|
+
mock_es_add_publication
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from fustor_demo.auto_generator import generator
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("fustor_demo")
|
|
23
|
+
|
|
24
|
+
# Get the directory where main.py is located
|
|
25
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
static_dir = os.path.join(current_dir, "static")
|
|
27
|
+
|
|
28
|
+
# Ensure static directory exists
|
|
29
|
+
if not os.path.exists(static_dir):
|
|
30
|
+
os.makedirs(static_dir)
|
|
31
|
+
|
|
32
|
+
app = FastAPI(
|
|
33
|
+
title="Fustor Bio-Fusion Demo API",
|
|
34
|
+
description="Demonstrates unified directory service from 5 heterogeneous data sources.",
|
|
35
|
+
version="0.1.0",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@app.on_event("startup")
|
|
39
|
+
async def startup_event():
|
|
40
|
+
await generator.start()
|
|
41
|
+
|
|
42
|
+
@app.on_event("shutdown")
|
|
43
|
+
async def shutdown_event():
|
|
44
|
+
await generator.stop()
|
|
45
|
+
|
|
46
|
+
# Mount static files to serve the frontend UI
|
|
47
|
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
48
|
+
|
|
49
|
+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
|
50
|
+
async def serve_demo_ui():
|
|
51
|
+
"""Serve the main HTML page for the demo UI."""
|
|
52
|
+
html_file_path = os.path.join(static_dir, "index.html")
|
|
53
|
+
if not os.path.exists(html_file_path):
|
|
54
|
+
raise HTTPException(status_code=404, detail="Demo UI (index.html) not found. Please ensure it's in the static directory.")
|
|
55
|
+
return FileResponse(html_file_path)
|
|
56
|
+
|
|
57
|
+
@app.get("/api/unified_directory", response_model=List[Dict[str, Any]])
|
|
58
|
+
async def get_unified_directory(
|
|
59
|
+
project_id: Optional[str] = None
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Query the unified directory service.
|
|
63
|
+
Returns all projects if project_id is None or "ALL".
|
|
64
|
+
Returns items within a specific project if project_id is provided.
|
|
65
|
+
"""
|
|
66
|
+
if project_id and project_id.upper() == "ALL":
|
|
67
|
+
return demo_store.get_unified_directory("ALL")
|
|
68
|
+
return demo_store.get_unified_directory(project_id)
|
|
69
|
+
|
|
70
|
+
@app.post("/api/mock/mysql_project")
|
|
71
|
+
async def trigger_mysql_project(project_name: str = "Project_Alpha"):
|
|
72
|
+
"""Trigger a mock MySQL event to create a new project."""
|
|
73
|
+
event = mock_mysql_create_project(project_name)
|
|
74
|
+
return {"status": "success", "message": f"Mock MySQL project '{project_name}' created.", "event": event}
|
|
75
|
+
|
|
76
|
+
@app.post("/api/mock/nfs_hot_file")
|
|
77
|
+
async def trigger_nfs_hot_file(project_id: str = "project_alpha", filename: str = "sample_data.fastq", size_mb: int = 100):
|
|
78
|
+
"""Trigger a mock NFS Hot event to add a file."""
|
|
79
|
+
event = mock_nfs_hot_add_file(project_id, filename, size_mb)
|
|
80
|
+
return {"status": "success", "message": f"Mock NFS hot file '{filename}' added to '{project_id}'.", "event": event}
|
|
81
|
+
|
|
82
|
+
@app.post("/api/mock/nfs_cold_file")
|
|
83
|
+
async def trigger_nfs_cold_file(project_id: str = "project_alpha", filename: str = "archive_data.bam", size_gb: int = 50):
|
|
84
|
+
"""Trigger a mock NFS Cold event to add an archived file."""
|
|
85
|
+
event = mock_nfs_cold_add_file(project_id, filename, size_gb)
|
|
86
|
+
return {"status": "success", "message": f"Mock NFS cold file '{filename}' added to '{project_id}'.", "event": event}
|
|
87
|
+
|
|
88
|
+
@app.post("/api/mock/oss_dataset")
|
|
89
|
+
async def trigger_oss_dataset(project_id: str = "project_alpha", dataset_name: str = "gsa_public_dataset.zip", public_url: str = "https://oss.example.com/data.zip"):
|
|
90
|
+
"""Trigger a mock OSS event to add a public dataset link."""
|
|
91
|
+
event = mock_oss_add_dataset_link(project_id, dataset_name, public_url)
|
|
92
|
+
return {"status": "success", "message": f"Mock OSS dataset '{dataset_name}' added to '{project_id}'.", "event": event}
|
|
93
|
+
|
|
94
|
+
@app.post("/api/mock/es_publication")
|
|
95
|
+
async def trigger_es_publication(project_id: str = "project_alpha", title: str = "Novel Gene Discovery", pubmed_id: str = "38000000"):
|
|
96
|
+
"""Trigger a mock Elasticsearch event to add a publication metadata."""
|
|
97
|
+
event = mock_es_add_publication(project_id, title, pubmed_id)
|
|
98
|
+
return {"status": "success", "message": f"Mock ES publication '{title}' added to '{project_id}'.", "event": event}
|
|
99
|
+
|
|
100
|
+
@app.post("/api/clear_store")
|
|
101
|
+
async def clear_store():
|
|
102
|
+
"""Clears all data in the demo store."""
|
|
103
|
+
demo_store.clear()
|
|
104
|
+
return {"status": "success", "message": "Demo store cleared."}
|
|
105
|
+
|
|
106
|
+
# Main entry point for uvicorn
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
import logging
|
|
109
|
+
import uvicorn
|
|
110
|
+
# Configure uvicorn to use DEBUG level for access logs to reduce verbosity
|
|
111
|
+
uvicorn_logger = logging.getLogger("uvicorn.access")
|
|
112
|
+
uvicorn_logger.setLevel(logging.DEBUG)
|
|
113
|
+
|
|
114
|
+
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
import uuid
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
from fustor_core.models.config import SourceConfig, PasswdCredential # Used for type hinting config in mock
|
|
7
|
+
from fustor_event_model.models import EventType # Used for event types in mock
|
|
8
|
+
from fustor_demo.store import demo_store
|
|
9
|
+
|
|
10
|
+
# --- Helper to generate common event structure ---
|
|
11
|
+
def _generate_base_event(
|
|
12
|
+
project_id: str,
|
|
13
|
+
source_type: str,
|
|
14
|
+
item_type: str, # "file", "directory", "link", "metadata"
|
|
15
|
+
name: str,
|
|
16
|
+
path: str,
|
|
17
|
+
size: Optional[int] = None,
|
|
18
|
+
url: Optional[str] = None,
|
|
19
|
+
extra_metadata: Optional[Dict[str, Any]] = None
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""Generates a base event dictionary for the demo store."""
|
|
22
|
+
unique_id = f"{source_type}-{project_id}-{uuid.uuid4().hex[:8]}"
|
|
23
|
+
return {
|
|
24
|
+
"id": unique_id,
|
|
25
|
+
"name": name,
|
|
26
|
+
"type": item_type,
|
|
27
|
+
"source_type": source_type,
|
|
28
|
+
"project_id": project_id,
|
|
29
|
+
"path": path,
|
|
30
|
+
"size": size,
|
|
31
|
+
"last_modified": datetime.now(timezone.utc).isoformat(),
|
|
32
|
+
"url": url,
|
|
33
|
+
"extra_metadata": extra_metadata or {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# --- Mock Agent Functions ---
|
|
37
|
+
|
|
38
|
+
def mock_mysql_create_project(project_name: str) -> Dict[str, Any]:
|
|
39
|
+
"""Simulates a MySQL Agent creating a new project."""
|
|
40
|
+
project_id = project_name.lower().replace(" ", "_")
|
|
41
|
+
event = _generate_base_event(
|
|
42
|
+
project_id=project_id,
|
|
43
|
+
source_type="mysql",
|
|
44
|
+
item_type="directory",
|
|
45
|
+
name=project_name,
|
|
46
|
+
path=f"/{project_name}",
|
|
47
|
+
extra_metadata={
|
|
48
|
+
"description": f"Research project {project_name}.",
|
|
49
|
+
"pi": f"Dr. {random.choice(['Smith', 'Jones', 'Li', 'Chen'])}",
|
|
50
|
+
"created_by": "System"
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
demo_store.add_event(event)
|
|
54
|
+
return event
|
|
55
|
+
|
|
56
|
+
def mock_nfs_hot_add_file(project_id: str, filename: str, size_mb: int) -> Dict[str, Any]:
|
|
57
|
+
"""Simulates an NFS Hot Agent detecting a new file."""
|
|
58
|
+
event = _generate_base_event(
|
|
59
|
+
project_id=project_id,
|
|
60
|
+
source_type="nfs_hot",
|
|
61
|
+
item_type="file",
|
|
62
|
+
name=filename,
|
|
63
|
+
path=f"/{project_id}/hot_data/{filename}",
|
|
64
|
+
size=size_mb * 1024 * 1024,
|
|
65
|
+
extra_metadata={
|
|
66
|
+
"local_path": f"/mnt/nfs_hot/{project_id}/{filename}",
|
|
67
|
+
"last_access": datetime.now(timezone.utc).isoformat()
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
demo_store.add_event(event)
|
|
71
|
+
return event
|
|
72
|
+
|
|
73
|
+
def mock_nfs_cold_add_file(project_id: str, filename: str, size_gb: int, age_days: int = 370) -> Dict[str, Any]:
|
|
74
|
+
"""Simulates an NFS Cold Agent detecting an archived file."""
|
|
75
|
+
archived_time = datetime.now(timezone.utc) - timedelta(days=age_days)
|
|
76
|
+
event = _generate_base_event(
|
|
77
|
+
project_id=project_id,
|
|
78
|
+
source_type="nfs_cold",
|
|
79
|
+
item_type="file",
|
|
80
|
+
name=filename,
|
|
81
|
+
path=f"/{project_id}/cold_archive/{filename}",
|
|
82
|
+
size=size_gb * 1024 * 1024 * 1024,
|
|
83
|
+
extra_metadata={
|
|
84
|
+
"local_path": f"/mnt/nfs_cold_archive/{project_id}/{filename}",
|
|
85
|
+
"archived_on": archived_time.isoformat(),
|
|
86
|
+
"retrieval_status": "offline"
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
demo_store.add_event(event)
|
|
90
|
+
return event
|
|
91
|
+
|
|
92
|
+
def mock_oss_add_dataset_link(project_id: str, dataset_name: str, public_url: str) -> Dict[str, Any]:
|
|
93
|
+
"""Simulates an OSS Agent detecting a new public dataset link."""
|
|
94
|
+
event = _generate_base_event(
|
|
95
|
+
project_id=project_id,
|
|
96
|
+
source_type="oss",
|
|
97
|
+
item_type="link",
|
|
98
|
+
name=dataset_name,
|
|
99
|
+
path=f"/{project_id}/public_datasets/{dataset_name}",
|
|
100
|
+
url=public_url,
|
|
101
|
+
extra_metadata={
|
|
102
|
+
"provider": "GSA/OMIX Public Data",
|
|
103
|
+
"version": "1.0",
|
|
104
|
+
"access_level": "public"
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
demo_store.add_event(event)
|
|
108
|
+
return event
|
|
109
|
+
|
|
110
|
+
def mock_es_add_publication(project_id: str, title: str, pubmed_id: str) -> Dict[str, Any]:
|
|
111
|
+
"""Simulates an Elasticsearch Agent finding a new publication metadata."""
|
|
112
|
+
event = _generate_base_event(
|
|
113
|
+
project_id=project_id,
|
|
114
|
+
source_type="es",
|
|
115
|
+
item_type="metadata",
|
|
116
|
+
name=title,
|
|
117
|
+
path=f"/{project_id}/publications/{pubmed_id}",
|
|
118
|
+
url=f"https://pubmed.ncbi.nlm.nih.gov/{pubmed_id}/",
|
|
119
|
+
extra_metadata={
|
|
120
|
+
"pubmed_id": pubmed_id,
|
|
121
|
+
"journal": "Science",
|
|
122
|
+
"authors": ["J. Doe", "A. Smith"],
|
|
123
|
+
"abstract_snippet": "This study investigates...",
|
|
124
|
+
"publication_date": (datetime.now(timezone.utc) - timedelta(days=random.randint(10, 300))).isoformat()
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
demo_store.add_event(event)
|
|
128
|
+
return event
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Fustor Bio-Fusion Demo - Unified Directory API Explorer</title>
|
|
7
|
+
<!-- Bootstrap CSS -->
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
9
|
+
<!-- Vue.js 3 -->
|
|
10
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
11
|
+
<!-- Axios for HTTP requests -->
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
13
|
+
<style>
|
|
14
|
+
body {
|
|
15
|
+
background-color: #212529; /* Dark background */
|
|
16
|
+
color: #f8f9fa; /* Light text */
|
|
17
|
+
}
|
|
18
|
+
.container-fluid {
|
|
19
|
+
padding-top: 20px;
|
|
20
|
+
}
|
|
21
|
+
.card {
|
|
22
|
+
background-color: #343a40; /* Darker card background */
|
|
23
|
+
border-color: #495057;
|
|
24
|
+
margin-bottom: 15px;
|
|
25
|
+
}
|
|
26
|
+
.card-header {
|
|
27
|
+
background-color: #495057;
|
|
28
|
+
color: #f8f9fa;
|
|
29
|
+
}
|
|
30
|
+
.btn-primary {
|
|
31
|
+
background-color: #007bff;
|
|
32
|
+
border-color: #007bff;
|
|
33
|
+
}
|
|
34
|
+
.btn-primary:hover {
|
|
35
|
+
background-color: #0056b3;
|
|
36
|
+
border-color: #0056b3;
|
|
37
|
+
}
|
|
38
|
+
.list-group-item {
|
|
39
|
+
background-color: #343a40;
|
|
40
|
+
color: #f8f9fa;
|
|
41
|
+
border-color: #495057;
|
|
42
|
+
}
|
|
43
|
+
.badge {
|
|
44
|
+
font-size: 0.75em;
|
|
45
|
+
padding: 0.4em 0.6em;
|
|
46
|
+
border-radius: 0.25rem;
|
|
47
|
+
}
|
|
48
|
+
.badge-mysql { background-color: #6c757d; } /* Grey */
|
|
49
|
+
.badge-nfs_hot { background-color: #dc3545; } /* Red */
|
|
50
|
+
.badge-nfs_cold { background-color: #17a2b8; } /* Cyan */
|
|
51
|
+
.badge-oss { background-color: #ffc107; } /* Yellow */
|
|
52
|
+
.badge-es { background-color: #28a745; } /* Green */
|
|
53
|
+
.badge-system { background-color: #007bff; } /* Blue */
|
|
54
|
+
|
|
55
|
+
.json-output {
|
|
56
|
+
background-color: #282c34;
|
|
57
|
+
color: #abb2bf;
|
|
58
|
+
padding: 15px;
|
|
59
|
+
border-radius: 5px;
|
|
60
|
+
overflow-x: auto;
|
|
61
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
62
|
+
}
|
|
63
|
+
pre { margin: 0; }
|
|
64
|
+
.log-entry {
|
|
65
|
+
font-size: 0.85em;
|
|
66
|
+
color: #adb5bd;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
69
|
+
</head>
|
|
70
|
+
<body>
|
|
71
|
+
<div id="app" class="container-fluid">
|
|
72
|
+
<h1 class="mb-4 text-center text-primary">Fustor Bio-Fusion Demo - Unified Directory API Explorer</h1>
|
|
73
|
+
<div class="row">
|
|
74
|
+
<!-- Left Panel: Source Controls -->
|
|
75
|
+
<div class="col-md-4">
|
|
76
|
+
<h3 class="mb-3">Source Controls (Simulated Agents)</h3>
|
|
77
|
+
<div class="card">
|
|
78
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
79
|
+
MySQL (Projects)
|
|
80
|
+
<span class="badge bg-success">Online</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="card-body">
|
|
83
|
+
<div class="input-group mb-3">
|
|
84
|
+
<input type="text" class="form-control" v-model="mysqlProjectName" placeholder="Project Name">
|
|
85
|
+
<button class="btn btn-primary" @click="triggerMock('mysql_project', {project_name: mysqlProjectName})">Create Project</button>
|
|
86
|
+
</div>
|
|
87
|
+
<small class="text-muted">New project: {{ mysqlProjectName }}</small>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="card">
|
|
92
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
93
|
+
NFS Hot (Raw Data)
|
|
94
|
+
<span class="badge bg-success">Online</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="card-body">
|
|
97
|
+
<div class="input-group mb-3">
|
|
98
|
+
<input type="text" class="form-control" v-model="nfsHotProjectId" placeholder="Project ID">
|
|
99
|
+
<input type="text" class="form-control" v-model="nfsHotFilename" placeholder="Filename">
|
|
100
|
+
<input type="number" class="form-control" v-model.number="nfsHotSize" placeholder="Size (MB)">
|
|
101
|
+
<button class="btn btn-primary" @click="triggerMock('nfs_hot_file', {project_id: nfsHotProjectId, filename: nfsHotFilename, size_mb: nfsHotSize})">Add File</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="card">
|
|
107
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
108
|
+
NFS Cold (Archived Data)
|
|
109
|
+
<span class="badge bg-success">Online</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="card-body">
|
|
112
|
+
<div class="input-group mb-3">
|
|
113
|
+
<input type="text" class="form-control" v-model="nfsColdProjectId" placeholder="Project ID">
|
|
114
|
+
<input type="text" class="form-control" v-model="nfsColdFilename" placeholder="Filename">
|
|
115
|
+
<input type="number" class="form-control" v-model.number="nfsColdSize" placeholder="Size (GB)">
|
|
116
|
+
<button class="btn btn-primary" @click="triggerMock('nfs_cold_file', {project_id: nfsColdProjectId, filename: nfsColdFilename, size_gb: nfsColdSize})">Archive File</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="card">
|
|
122
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
123
|
+
OSS (Public Datasets)
|
|
124
|
+
<span class="badge bg-success">Online</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="card-body">
|
|
127
|
+
<div class="input-group mb-3">
|
|
128
|
+
<input type="text" class="form-control" v-model="ossProjectId" placeholder="Project ID">
|
|
129
|
+
<input type="text" class="form-control" v-model="ossDatasetName" placeholder="Dataset Name">
|
|
130
|
+
<input type="text" class="form-control" v-model="ossPublicUrl" placeholder="Public URL">
|
|
131
|
+
<button class="btn btn-primary" @click="triggerMock('oss_dataset', {project_id: ossProjectId, dataset_name: ossDatasetName, public_url: ossPublicUrl})">Add Dataset Link</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="card">
|
|
137
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
138
|
+
ES (PubMed Publications)
|
|
139
|
+
<span class="badge bg-success">Online</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="card-body">
|
|
142
|
+
<div class="input-group mb-3">
|
|
143
|
+
<input type="text" class="form-control" v-model="esProjectId" placeholder="Project ID">
|
|
144
|
+
<input type="text" class="form-control" v-model="esTitle" placeholder="Title">
|
|
145
|
+
<input type="text" class="form-control" v-model="esPubmedId" placeholder="PubMed ID">
|
|
146
|
+
<button class="btn btn-primary" @click="triggerMock('es_publication', {project_id: esProjectId, title: esTitle, pubmed_id: esPubmedId})">Add Publication</button>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="d-grid gap-2">
|
|
151
|
+
<button class="btn btn-warning" @click="clearStore">Clear All Data</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<!-- Right Panel: Unified Directory API Explorer -->
|
|
156
|
+
<div class="col-md-8">
|
|
157
|
+
<h3 class="mb-3">Unified Directory API Explorer</h3>
|
|
158
|
+
<div class="card mb-3">
|
|
159
|
+
<div class="card-header">API Endpoint</div>
|
|
160
|
+
<div class="card-body">
|
|
161
|
+
<div class="input-group">
|
|
162
|
+
<span class="input-group-text">GET</span>
|
|
163
|
+
<input type="text" class="form-control" :value="apiEndpoint" readonly>
|
|
164
|
+
<select class="form-select" v-model="selectedProjectId" @change="fetchData">
|
|
165
|
+
<option value="ALL">ALL Projects</option>
|
|
166
|
+
<option v-for="proj in currentProjects" :value="proj.id">{{ proj.name }}</option>
|
|
167
|
+
</select>
|
|
168
|
+
<button class="btn btn-primary" @click="fetchData">Refresh</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
174
|
+
<h4>API Response</h4>
|
|
175
|
+
<div>
|
|
176
|
+
<button class="btn btn-sm btn-outline-light me-2" @click="toggleView('table')" :class="{'active': currentView === 'table'}">Table View</button>
|
|
177
|
+
<button class="btn btn-sm btn-outline-light" @click="toggleView('json')" :class="{'active': currentView === 'json'}">JSON View</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div v-if="loading" class="text-center text-info mb-3">Loading...</div>
|
|
182
|
+
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
|
183
|
+
|
|
184
|
+
<div v-if="currentView === 'table'" class="table-responsive">
|
|
185
|
+
<table class="table table-dark table-striped table-hover">
|
|
186
|
+
<thead>
|
|
187
|
+
<tr>
|
|
188
|
+
<th>Name</th>
|
|
189
|
+
<th>Type</th>
|
|
190
|
+
<th>Logical Path</th>
|
|
191
|
+
<th>Source</th>
|
|
192
|
+
<th>Size</th>
|
|
193
|
+
<th>Last Modified</th>
|
|
194
|
+
<th>URL / Details</th>
|
|
195
|
+
</tr>
|
|
196
|
+
</thead>
|
|
197
|
+
<tbody>
|
|
198
|
+
<tr v-if="!unifiedDirectory.length && !loading">
|
|
199
|
+
<td colspan="7" class="text-center text-muted">No data available. Create some projects and items!</td>
|
|
200
|
+
</tr>
|
|
201
|
+
<template v-for="item in sortedDirectory" :key="item.id">
|
|
202
|
+
<tr :class="{'table-info': item.isNew}">
|
|
203
|
+
<td>
|
|
204
|
+
<i :class="getIcon(item.type)" class="me-2"></i>
|
|
205
|
+
{{ item.name }}
|
|
206
|
+
</td>
|
|
207
|
+
<td>
|
|
208
|
+
<span :class="getBadgeClass(item.type)">{{ item.type }}</span>
|
|
209
|
+
</td>
|
|
210
|
+
<td>{{ item.path }}</td>
|
|
211
|
+
<td>
|
|
212
|
+
<span :class="getBadgeClass(item.source_type)">{{ item.source_type }}</span>
|
|
213
|
+
</td>
|
|
214
|
+
<td>{{ formatSize(item.size) }}</td>
|
|
215
|
+
<td>{{ formatDateTime(item.last_modified) }}</td>
|
|
216
|
+
<td>
|
|
217
|
+
<a v-if="item.url" :href="item.url" target="_blank" class="text-info">{{ item.url.length > 30 ? item.url.substring(0,27) + '...' : item.url }}</a>
|
|
218
|
+
<span v-else-if="item.extra_metadata">{{ JSON.stringify(item.extra_metadata).substring(0, 50) + '...' }}</span>
|
|
219
|
+
<span v-else>-</span>
|
|
220
|
+
</td>
|
|
221
|
+
</tr>
|
|
222
|
+
</template>
|
|
223
|
+
</tbody>
|
|
224
|
+
</table>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div v-if="currentView === 'json'" class="json-output">
|
|
228
|
+
<pre>{{ JSON.stringify(unifiedDirectory, null, 2) }}</pre>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<script>
|
|
235
|
+
const app = Vue.createApp({
|
|
236
|
+
data() {
|
|
237
|
+
return {
|
|
238
|
+
apiBaseUrl: window.location.origin,
|
|
239
|
+
unifiedDirectory: [],
|
|
240
|
+
currentProjects: [],
|
|
241
|
+
selectedProjectId: 'ALL',
|
|
242
|
+
loading: false,
|
|
243
|
+
error: null,
|
|
244
|
+
currentView: 'table', // 'table' or 'json'
|
|
245
|
+
|
|
246
|
+
// Mock agent input fields
|
|
247
|
+
mysqlProjectName: 'Project_Gamma',
|
|
248
|
+
nfsHotProjectId: 'project_alpha',
|
|
249
|
+
nfsHotFilename: 'sequencing_results.fastq',
|
|
250
|
+
nfsHotSize: 100,
|
|
251
|
+
nfsColdProjectId: 'project_alpha',
|
|
252
|
+
nfsColdFilename: 'archived_run_001.bam',
|
|
253
|
+
nfsColdSize: 50,
|
|
254
|
+
ossProjectId: 'project_alpha',
|
|
255
|
+
ossDatasetName: 'public_ref_genome.fasta',
|
|
256
|
+
ossPublicUrl: 'https://public-data.oss.com/ref_genome.fasta',
|
|
257
|
+
esProjectId: 'project_alpha',
|
|
258
|
+
esTitle: 'New Cancer Marker Discovery',
|
|
259
|
+
esPubmedId: '38123456',
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
computed: {
|
|
263
|
+
apiEndpoint() {
|
|
264
|
+
let endpoint = `${this.apiBaseUrl}/api/unified_directory`;
|
|
265
|
+
if (this.selectedProjectId) {
|
|
266
|
+
endpoint += `?project_id=${this.selectedProjectId}`;
|
|
267
|
+
}
|
|
268
|
+
return endpoint;
|
|
269
|
+
},
|
|
270
|
+
sortedDirectory() {
|
|
271
|
+
const projects = this.unifiedDirectory.filter(item => item.type === 'directory');
|
|
272
|
+
const items = this.unifiedDirectory.filter(item => item.type !== 'directory');
|
|
273
|
+
|
|
274
|
+
let sortedItems = items.sort((a, b) => {
|
|
275
|
+
const pathA = a.path || a.name;
|
|
276
|
+
const pathB = b.path || b.name;
|
|
277
|
+
return pathA.localeCompare(pathB);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// If "ALL Projects" is selected, group by project_id and display projects first
|
|
281
|
+
if (this.selectedProjectId === 'ALL') {
|
|
282
|
+
let result = [];
|
|
283
|
+
projects.forEach(proj => {
|
|
284
|
+
result.push(proj);
|
|
285
|
+
result.push(...sortedItems.filter(item => item.project_id === proj.id));
|
|
286
|
+
});
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
return sortedItems;
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
methods: {
|
|
293
|
+
async fetchData() {
|
|
294
|
+
this.loading = true;
|
|
295
|
+
this.error = null;
|
|
296
|
+
try {
|
|
297
|
+
const response = await axios.get(this.apiEndpoint);
|
|
298
|
+
this.unifiedDirectory = response.data;
|
|
299
|
+
this.updateProjectList();
|
|
300
|
+
} catch (err) {
|
|
301
|
+
this.error = 'Failed to fetch data: ' + (err.response?.data?.detail || err.message);
|
|
302
|
+
} finally {
|
|
303
|
+
this.loading = false;
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
async triggerMock(endpoint, data) {
|
|
307
|
+
this.loading = true;
|
|
308
|
+
this.error = null;
|
|
309
|
+
try {
|
|
310
|
+
const response = await axios.post(`${this.apiBaseUrl}/api/mock/${endpoint}`, data);
|
|
311
|
+
console.log(`Triggered mock ${endpoint}:`, response.data);
|
|
312
|
+
// Refresh data after trigger
|
|
313
|
+
await this.fetchData();
|
|
314
|
+
} catch (err) {
|
|
315
|
+
this.error = 'Failed to trigger mock event: ' + (err.response?.data?.detail || err.message);
|
|
316
|
+
} finally {
|
|
317
|
+
this.loading = false;
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
async clearStore() {
|
|
321
|
+
this.loading = true;
|
|
322
|
+
this.error = null;
|
|
323
|
+
try {
|
|
324
|
+
await axios.post(`${this.apiBaseUrl}/api/clear_store`);
|
|
325
|
+
await this.fetchData();
|
|
326
|
+
alert('Demo store cleared!');
|
|
327
|
+
} catch (err) {
|
|
328
|
+
this.error = 'Failed to clear store: ' + (err.response?.data?.detail || err.message);
|
|
329
|
+
} finally {
|
|
330
|
+
this.loading = false;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
toggleView(view) {
|
|
334
|
+
this.currentView = view;
|
|
335
|
+
},
|
|
336
|
+
formatSize(bytes) {
|
|
337
|
+
if (bytes === null || bytes === undefined) return '-';
|
|
338
|
+
if (bytes === 0) return '0 Bytes';
|
|
339
|
+
const k = 1024;
|
|
340
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
341
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
342
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
343
|
+
},
|
|
344
|
+
formatDateTime(isoString) {
|
|
345
|
+
if (!isoString) return '-';
|
|
346
|
+
const date = new Date(isoString);
|
|
347
|
+
return date.toLocaleString();
|
|
348
|
+
},
|
|
349
|
+
getBadgeClass(sourceType) {
|
|
350
|
+
return `badge badge-${sourceType.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
351
|
+
},
|
|
352
|
+
getIcon(itemType) {
|
|
353
|
+
switch (itemType) {
|
|
354
|
+
case 'directory': return 'bi bi-folder-fill'; // Bootstrap Icons
|
|
355
|
+
case 'file': return 'bi bi-file-earmark';
|
|
356
|
+
case 'link': return 'bi bi-link-45deg';
|
|
357
|
+
case 'metadata': return 'bi bi-journal-richtext';
|
|
358
|
+
default: return 'bi bi-question-circle';
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
updateProjectList() {
|
|
362
|
+
// Filter out unique projects that are of type 'directory'
|
|
363
|
+
const uniqueProjects = {};
|
|
364
|
+
this.unifiedDirectory.forEach(item => {
|
|
365
|
+
if (item.type === 'directory') {
|
|
366
|
+
uniqueProjects[item.id] = { id: item.id, name: item.name };
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
this.currentProjects = Object.values(uniqueProjects);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
mounted() {
|
|
373
|
+
this.fetchData();
|
|
374
|
+
// Fetch data every 2 seconds to simulate real-time updates
|
|
375
|
+
setInterval(this.fetchData, 2000);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
app.mount('#app');
|
|
380
|
+
</script>
|
|
381
|
+
<!-- Bootstrap Icons (Optional, for better icons) -->
|
|
382
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
|
383
|
+
<!-- Bootstrap JS (Optional, if you need interactive components like modals) -->
|
|
384
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
385
|
+
</body>
|
|
386
|
+
</html>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
class DemoStore:
|
|
6
|
+
"""
|
|
7
|
+
A simple in-memory store for the demo.
|
|
8
|
+
Aggregates events into a unified directory structure, mimicking Fusion's role.
|
|
9
|
+
"""
|
|
10
|
+
def __init__(self):
|
|
11
|
+
# Stores projects, indexed by project_id
|
|
12
|
+
# Example: { "project_alpha": { "id": "project_alpha", "name": "Project Alpha", "files": [], "metadata": [] } }
|
|
13
|
+
self._projects: Dict[str, Dict[str, Any]] = {}
|
|
14
|
+
self._lock = {} # Placeholder for thread-safety in a real app, simplified for demo
|
|
15
|
+
|
|
16
|
+
def add_event(self, event_data: Dict[str, Any]):
|
|
17
|
+
"""
|
|
18
|
+
Adds an event to the store, aggregating it by project_id.
|
|
19
|
+
Expected event_data structure:
|
|
20
|
+
{
|
|
21
|
+
"id": "unique_id_for_this_item",
|
|
22
|
+
"name": "display_name",
|
|
23
|
+
"type": "file" | "directory" | "link" | "metadata",
|
|
24
|
+
"source_type": "mysql" | "nfs_hot" | "nfs_cold" | "oss" | "es",
|
|
25
|
+
"project_id": "bio_project_id",
|
|
26
|
+
"path": "/logical/path/to/item",
|
|
27
|
+
"size": "1024" | None,
|
|
28
|
+
"last_modified": "ISO_timestamp",
|
|
29
|
+
"url": "http://download.link" | None,
|
|
30
|
+
"extra_metadata": {}
|
|
31
|
+
}
|
|
32
|
+
"""
|
|
33
|
+
project_id = event_data.get("project_id")
|
|
34
|
+
if not project_id:
|
|
35
|
+
# For events without a project_id, put them under a "Unassigned" project
|
|
36
|
+
project_id = "unassigned"
|
|
37
|
+
event_data["project_id"] = project_id
|
|
38
|
+
|
|
39
|
+
if project_id not in self._projects:
|
|
40
|
+
self._projects[project_id] = {
|
|
41
|
+
"id": project_id,
|
|
42
|
+
"name": event_data.get("project_name", project_id.replace("_", " ").title()),
|
|
43
|
+
"type": "directory",
|
|
44
|
+
"source_type": "mysql" if project_id != "unassigned" else "system", # Assume projects are from MySQL
|
|
45
|
+
"path": f"/{project_id}",
|
|
46
|
+
"items": [],
|
|
47
|
+
"last_modified": datetime.now(timezone.utc).isoformat()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Add the item to the project's items list
|
|
51
|
+
# Ensure it's not a duplicate based on event_data['id']
|
|
52
|
+
# This is a very simplified deduplication. In a real app, you'd update existing items.
|
|
53
|
+
existing_item_ids = {item["id"] for item in self._projects[project_id]["items"]}
|
|
54
|
+
if event_data["id"] not in existing_item_ids:
|
|
55
|
+
self._projects[project_id]["items"].append(copy.deepcopy(event_data))
|
|
56
|
+
# Sort items for consistent display
|
|
57
|
+
self._projects[project_id]["items"].sort(key=lambda x: x.get("path", x.get("name", "")))
|
|
58
|
+
else:
|
|
59
|
+
# Update existing item
|
|
60
|
+
for i, item in enumerate(self._projects[project_id]["items"]):
|
|
61
|
+
if item["id"] == event_data["id"]:
|
|
62
|
+
self._projects[project_id]["items"][i] = copy.deepcopy(event_data)
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_unified_directory(self, project_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
67
|
+
"""
|
|
68
|
+
Retrieves the unified directory structure.
|
|
69
|
+
If project_id is None, returns all top-level projects.
|
|
70
|
+
If project_id is specified, returns items within that project.
|
|
71
|
+
"""
|
|
72
|
+
if project_id == "ALL": # Special case for all top-level projects
|
|
73
|
+
# Return top-level projects, sorted by name
|
|
74
|
+
return sorted(
|
|
75
|
+
[copy.deepcopy(proj) for proj in self._projects.values() if proj["type"] == "directory"],
|
|
76
|
+
key=lambda x: x["name"]
|
|
77
|
+
)
|
|
78
|
+
elif project_id and project_id in self._projects:
|
|
79
|
+
# Return items within a specific project
|
|
80
|
+
return sorted(
|
|
81
|
+
copy.deepcopy(self._projects[project_id]["items"]),
|
|
82
|
+
key=lambda x: x.get("path", x.get("name", ""))
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
# Return all items if no project specified, or for invalid project_id
|
|
86
|
+
all_items = []
|
|
87
|
+
for proj in self._projects.values():
|
|
88
|
+
all_items.append(copy.deepcopy(proj))
|
|
89
|
+
all_items.extend(copy.deepcopy(proj["items"]))
|
|
90
|
+
return sorted(
|
|
91
|
+
all_items,
|
|
92
|
+
key=lambda x: x.get("path", x.get("name", ""))
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def clear(self):
|
|
96
|
+
"""Clears all stored data."""
|
|
97
|
+
self._projects = {}
|
|
98
|
+
|
|
99
|
+
# Instantiate a global store for the demo server
|
|
100
|
+
demo_store = DemoStore()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-demo
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: Fustor Bio-Fusion Demo for unified directory service
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: fastapi[all]>=0.104.1
|
|
7
|
+
Requires-Dist: uvicorn>=0.23.2
|
|
8
|
+
Requires-Dist: fustor-core
|
|
9
|
+
Requires-Dist: fustor-event-model
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: httpx>=0.25.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/fustor_demo/auto_generator.py
|
|
3
|
+
src/fustor_demo/main.py
|
|
4
|
+
src/fustor_demo/mock_agents.py
|
|
5
|
+
src/fustor_demo/store.py
|
|
6
|
+
src/fustor_demo.egg-info/PKG-INFO
|
|
7
|
+
src/fustor_demo.egg-info/SOURCES.txt
|
|
8
|
+
src/fustor_demo.egg-info/dependency_links.txt
|
|
9
|
+
src/fustor_demo.egg-info/requires.txt
|
|
10
|
+
src/fustor_demo.egg-info/top_level.txt
|
|
11
|
+
src/fustor_demo/static/index.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fustor_demo
|