11import os
2- from typing import Any , Callable , Dict , Optional , List
2+ import inspect
3+ from typing import Any , Callable , Dict , List , Union
4+
35
46# --- Optional Server Dependencies ---
57try :
6- from fastapi import FastAPI , APIRouter , Request
7- from fastapi .responses import HTMLResponse , Response
8+ from fastapi import FastAPI , APIRouter , Request , Response
9+ from fastapi .responses import HTMLResponse
810 from fastapi .staticfiles import StaticFiles
9-
11+ import uvicorn
1012 HAS_SERVER = True
1113except ImportError :
1214 HAS_SERVER = False
13- # Dummy classes for type hinting if dependencies are missing
14- # FastAPI = object # type: ignore
15- # APIRouter = object # type: ignore
16- # Request = object # type: ignore
15+ # Dummy classes for type hinting
16+ FastAPI = object # type: ignore
17+ APIRouter = object # type: ignore
18+ Request = object # type: ignore
19+ Response = object # type: ignore
20+
21+
22+ from .stylesheet import StyleSheet
23+ from .markup import Document , StyleResource
1724
1825
1926class App :
@@ -26,27 +33,78 @@ def __init__(self, title: str = "Violetear App"):
2633 if not HAS_SERVER :
2734 raise ImportError (
2835 "Violetear Server dependencies are missing. "
29- "Please install them using `pip install violetear[ server]` "
36+ "Please install them with: uv add --extra server 'fastapi[standard]' "
3037 )
3138
3239 self .title = title
3340 self .api = FastAPI (title = title )
34- self ._routes : List [Dict [str , Any ]] = []
3541
36- # We will add the Asset Registry here in Phase 2.2
37- self .styles = {}
42+ # Registry of served styles to prevent duplicate route registration
43+ self .served_styles : Dict [str , StyleSheet ] = {}
44+
45+ def add_style (self , path : str , sheet : StyleSheet ):
46+ """
47+ Registers a stylesheet to be served by the app at a specific path.
48+
49+ Overrides any previous stylesheet at that path.
50+ """
51+ if path not in self .served_styles :
52+ # Register the route dynamically (just once)
53+ @self .api .get (path )
54+ def serve_css ():
55+ # Render the full CSS content
56+ css_content = self .served_styles [path ]
57+ return Response (content = css_content , media_type = "text/css" )
58+
59+ # Set the stylesheet, overrides if existing
60+ # This means we can change stylesheets dynamically
61+ self .served_styles [path ] = sheet
62+
63+ def _register_document_styles (self , doc : Document ):
64+ """
65+ Scans a Document for external stylesheets defined in Python
66+ and registers their routes on the fly.
67+ """
68+ for resource in doc .head .styles :
69+ if isinstance (resource , StyleResource ):
70+ # If it has a sheet object AND a URL, it needs to be served
71+ if resource .sheet and resource .href and not resource .inline :
72+ self .add_style (resource .href , resource .sheet )
3873
3974 def route (self , path : str , methods : List [str ] = ["GET" ]):
4075 """
4176 Decorator to register a route.
42- Supports standard SSR (returning Documents) out of the box.
4377 """
44-
4578 def decorator (func : Callable ):
46- # We will implement the wrapper logic in Phase 2.2
47- self .api .add_api_route (path , func , methods = methods )
48- return func
79+ @self .api .api_route (path , methods = methods )
80+ async def wrapper (request : Request ):
81+ # 1. Handle Request (POST/GET)
82+ if request .method == "POST" :
83+ form_data = await request .form ()
84+ # Simple check if function accepts arguments
85+ if inspect .signature (func ).parameters :
86+ response = func (form_data )
87+ else :
88+ response = func ()
89+ else :
90+ response = func ()
91+
92+ # Await if async
93+ if inspect .isawaitable (response ):
94+ response = await response
4995
96+ # 2. Handle Document Rendering
97+ if isinstance (response , Document ):
98+ # JIT: Check if this doc uses any new stylesheets we need to serve
99+ self ._register_document_styles (response )
100+
101+ # Render the HTML (which will contain <link href="..."> tags)
102+ return HTMLResponse (response .render ())
103+
104+ # 3. Return raw response (JSON, Dict, etc.)
105+ return response
106+
107+ return wrapper
50108 return decorator
51109
52110 def mount_static (self , directory : str , path : str = "/static" ):
@@ -56,6 +114,4 @@ def mount_static(self, directory: str, path: str = "/static"):
56114
57115 def run (self , host = "0.0.0.0" , port = 8000 , ** kwargs ):
58116 """Helper to run via uvicorn programmatically."""
59- import uvicorn
60-
61117 uvicorn .run (self .api , host = host , port = port , ** kwargs )
0 commit comments