Skip to content

Commit b17fc2e

Browse files
committed
feat: init
1 parent e6fab60 commit b17fc2e

18 files changed

+1583
-1
lines changed

.gitignore

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,65 @@
1+
#######################################################
2+
# Python
3+
#######################################################
4+
5+
data/
6+
logs/
7+
settings.json
8+
ignore.txt
9+
.vscode
10+
.git
11+
makefile
12+
.ruff_cache/
13+
*.dat
14+
15+
# Python bytecode / Byte-compiled / optimized / DLL files
16+
__pycache__/
17+
__pypackages__/
18+
*.pyc
19+
*.pyo
20+
*.pyd
21+
/.venv/
22+
*.py[cod]
23+
*$py.class
24+
25+
# Local Poetry artifact cache
26+
/.cache/pypoetry/
27+
28+
# Unit test / coverage reports
29+
htmlcov/
30+
.tox/
31+
.nox/
32+
.coverage
33+
.coverage.*
34+
.cache
35+
nosetests.xml
36+
coverage.xml
37+
*.cover
38+
*.py,cover
39+
.hypothesis/
40+
.pytest_cache/
41+
cover/
42+
43+
# Environments
44+
*.env
45+
*.venv
46+
env/
47+
venv/
48+
ENV/
49+
env.bak/
50+
venv.bak/
51+
52+
# Rider IDE
53+
**/.idea/
54+
55+
# MacOs
56+
**/.DS_Store
57+
58+
59+
#######################################################
60+
# Node.js
61+
#######################################################
62+
163
# Logs
264
logs
365
*.log
@@ -102,7 +164,6 @@ dist
102164

103165
# vuepress v2.x temp and cache directory
104166
.temp
105-
.cache
106167

107168
# Docusaurus cache and generated files
108169
.docusaurus

poetry.lock

+998
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[tool.poetry]
2+
name = "thresh"
3+
version = "0.1.0"
4+
description = "WIP"
5+
authors = ["RivenMedia"]
6+
readme = "README.md"
7+
8+
[tool.poetry.dependencies]
9+
python = "^3.11"
10+
scalar-fastapi = "^1.0.3"
11+
fastapi = "^0.115.0"
12+
requests = "^2.32.3"
13+
asyncio = "^3.4.3"
14+
rank-torrent-name = "^1.2.1"
15+
parsett = "^1.3.1"
16+
pydantic = "^2.9.2"
17+
pydantic-settings = "^2.5.2"
18+
uvicorn = "^0.30.6"
19+
loguru = "^0.7.2"
20+
rich = "^13.8.1"
21+
22+
[tool.poetry.group.dev.dependencies]
23+
isort = "^5.13.2"
24+
25+
[build-system]
26+
requires = ["poetry-core"]
27+
build-backend = "poetry.core.masonry.api"

src/__init__.py

Whitespace-only changes.

src/controllers/__init__.py

Whitespace-only changes.

src/controllers/default.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from fastapi import APIRouter, HTTPException, Request, UploadFile
2+
from RTN import parse
3+
from PTT import parse_title
4+
5+
router = APIRouter(
6+
prefix="",
7+
tags=["default"],
8+
)
9+
10+
11+
@router.get(
12+
"/",
13+
summary="Root endpoint",
14+
description="This endpoint is the root of the API. It is used to check if the API is running.",
15+
include_in_schema=False,
16+
)
17+
async def read_root():
18+
return {"message": "Welcome to Thresh!"}
19+
20+
21+
@router.post("/parse")
22+
async def parse_post(request: Request, filename: str, raw: bool = False):
23+
"""
24+
Parse a file and return the parsed content.
25+
"""
26+
return parse_title(filename) if raw else parse(filename, translate_langs=True)

src/controllers/flow.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from fastapi import APIRouter, HTTPException, Request
2+
from pydantic import BaseModel
3+
from typing import List, Dict, Any
4+
5+
router = APIRouter(
6+
prefix="/flow",
7+
tags=["flow"],
8+
)
9+
10+
class Node(BaseModel):
11+
id: str
12+
type: str
13+
14+
class Edge(BaseModel):
15+
source: str
16+
target: str
17+
18+
class FlowData(BaseModel):
19+
nodes: List[Node]
20+
edges: List[Edge]
21+
22+
class FlowRequest(BaseModel):
23+
flow_data: FlowData
24+
initial_input: Any
25+
26+
class FlowResponse(BaseModel):
27+
result: Any
28+
29+
class ErrorResponse(BaseModel):
30+
detail: str
31+
32+
def execute_node(node_type, input_data):
33+
# This function would contain the logic for each node type
34+
if node_type == 'inputNode':
35+
return input_data
36+
elif node_type == 'multiplyNode':
37+
return input_data * 2
38+
elif node_type == 'addNode':
39+
return input_data + 10
40+
# Add more node types as needed
41+
42+
def process_flow(flow_data, initial_input):
43+
nodes = {node.id: node for node in flow_data.nodes}
44+
edges = flow_data.edges
45+
46+
# Create a dictionary to store the output of each node
47+
node_outputs = {}
48+
49+
# Process nodes in topological order (simplified for this example)
50+
for node in flow_data.nodes:
51+
node_id = node.id
52+
node_type = node.type
53+
54+
# Find input edges for this node
55+
input_edges = [edge for edge in edges if edge.target == node_id]
56+
57+
if not input_edges:
58+
# This is an input node
59+
node_outputs[node_id] = execute_node(node_type, initial_input)
60+
else:
61+
# Get the output from the source node of the first input edge
62+
input_data = node_outputs[input_edges[0].source]
63+
node_outputs[node_id] = execute_node(node_type, input_data)
64+
65+
# Return the output of the last node (assuming it's the output node)
66+
return node_outputs[flow_data.nodes[-1].id]
67+
68+
@router.post("/process", response_model=FlowResponse, responses={400: {"model": ErrorResponse}, 500: {"model": ErrorResponse}})
69+
async def process_flow_endpoint(flow_request: FlowRequest):
70+
"""
71+
Process a flow and return the result.
72+
73+
Example:
74+
```
75+
curl -X 'POST' \
76+
'http://localhost:8000/flow/process' \
77+
-H 'accept: application/json' \
78+
-H 'Content-Type: application/json' \
79+
-d '{
80+
"flow_data": {
81+
"nodes": [
82+
{"id": "1", "type": "inputNode"},
83+
{"id": "2", "type": "multiplyNode"},
84+
{"id": "3", "type": "addNode"}
85+
],
86+
"edges": [
87+
{"source": "1", "target": "2"},
88+
{"source": "2", "target": "3"}
89+
]
90+
},
91+
"initial_input": 5
92+
}'
93+
```
94+
"""
95+
try:
96+
flow_data = flow_request.flow_data
97+
initial_input = flow_request.initial_input
98+
99+
if not flow_data or initial_input is None:
100+
raise HTTPException(status_code=400, detail="Invalid input data")
101+
102+
result = process_flow(flow_data, initial_input)
103+
return {"result": result}
104+
except Exception as e:
105+
raise HTTPException(status_code=500, detail=str(e))

src/main.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import contextlib
2+
import signal
3+
import sys
4+
import threading
5+
import time
6+
import traceback
7+
8+
import uvicorn
9+
from fastapi import FastAPI
10+
from fastapi.middleware.cors import CORSMiddleware
11+
from scalar_fastapi import get_scalar_api_reference
12+
from starlette.middleware.base import BaseHTTPMiddleware
13+
from starlette.requests import Request
14+
15+
from controllers.default import router as default_router
16+
from controllers.flow import router as flow_router
17+
from program.program import Program
18+
from utils.logger import logger
19+
20+
21+
class LoguruMiddleware(BaseHTTPMiddleware):
22+
async def dispatch(self, request: Request, call_next):
23+
start_time = time.time()
24+
try:
25+
response = await call_next(request)
26+
except Exception as e:
27+
logger.exception(f"Exception during request processing: {e}")
28+
raise
29+
finally:
30+
process_time = time.time() - start_time
31+
logger.log(
32+
"API",
33+
f"{request.method} {request.url.path} - {response.status_code if 'response' in locals() else '500'} - {process_time:.2f}s",
34+
)
35+
return response
36+
37+
38+
app = FastAPI(
39+
title="Thresh",
40+
summary="A node based UI for use with Riven.",
41+
version="0.1.0", # TODO: add get_version()
42+
redoc_url=None,
43+
license_info={
44+
"name": "GPL-3.0",
45+
"url": "https://www.gnu.org/licenses/gpl-3.0.en.html",
46+
},
47+
)
48+
49+
@app.get("/scalar", include_in_schema=False)
50+
async def scalar_html():
51+
return get_scalar_api_reference(
52+
openapi_url=app.openapi_url,
53+
title=app.title,
54+
)
55+
56+
57+
app.add_middleware(LoguruMiddleware)
58+
app.add_middleware(
59+
CORSMiddleware,
60+
allow_origins=["*"],
61+
allow_credentials=True,
62+
allow_methods=["*"],
63+
allow_headers=["*"],
64+
)
65+
66+
app.include_router(default_router)
67+
app.include_router(flow_router)
68+
69+
app.program = Program()
70+
71+
class Server(uvicorn.Server):
72+
def install_signal_handlers(self):
73+
pass
74+
75+
@contextlib.contextmanager
76+
def run_in_thread(self):
77+
thread = threading.Thread(target=self.run, name="Riven")
78+
thread.start()
79+
try:
80+
while not self.started:
81+
time.sleep(1e-3)
82+
yield
83+
except Exception as e:
84+
logger.error(f"Error in server thread: {e}")
85+
logger.exception(traceback.format_exc())
86+
raise e
87+
finally:
88+
self.should_exit = True
89+
sys.exit(0)
90+
91+
def signal_handler(signum, frame):
92+
logger.log("PROGRAM","Exiting Gracefully.")
93+
app.program.stop()
94+
sys.exit(0)
95+
96+
signal.signal(signal.SIGINT, signal_handler)
97+
signal.signal(signal.SIGTERM, signal_handler)
98+
99+
config = uvicorn.Config(app, host="0.0.0.0", port=5359, log_config=None)
100+
server = Server(config=config)
101+
102+
with server.run_in_thread():
103+
try:
104+
app.program.start()
105+
app.program.run()
106+
except Exception as e:
107+
logger.error(f"Error in main thread: {e}")
108+
logger.exception(traceback.format_exc())
109+
finally:
110+
logger.critical("Server has been stopped")
111+
sys.exit(0)

src/program/__init__.py

Whitespace-only changes.

src/program/program.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import time
2+
3+
from loguru import logger
4+
5+
6+
class Program:
7+
def __init__(self):
8+
self.initialized = False
9+
self.running = False
10+
11+
def start(self):
12+
self.running = True
13+
logger.log("API", "API available at http://localhost:5359")
14+
logger.log("PROGRAM", "Program started")
15+
16+
def run(self):
17+
while self.running:
18+
time.sleep(1)
19+
20+
def stop(self):
21+
self.running = False
22+
logger.log("PROGRAM", "Program stopped")

src/settings/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)