Skip to content

Commit 2ebe86a

Browse files
committed
major improvements to Fast_API__Route__Extractor to take into account a couple bugs found in the client implementation brief
added classes Schema__Fast__API__Tags__Classes_And_Routes and Schema__Fast__API__Tag__Classes_And_Routes
1 parent b43998f commit 2ebe86a

10 files changed

Lines changed: 358 additions & 324 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Important mismatch between the FastAPI client implementation and the original schema design.
2+
3+
## Changes Not Accounted for in Original Brief
4+
5+
### 1. Multiple HTTP Methods per Endpoint
6+
- **Original assumption**: One method per endpoint
7+
- **Reality**: FastAPI routes can accept multiple HTTP methods
8+
- **Solution**: Generate separate client methods with HTTP verb prefixes (e.g., `get__list_users`, `post__list_users`)
9+
10+
### 2. Multiple Tags per Route
11+
- **Original assumption**: Single tag per route (`route_tag: Safe_Str__Fast_API__Route__Tag`)
12+
- **Reality**: FastAPI supports multiple tags per route for organization/documentation
13+
- **Solution**: Change to `route_tags: List[Safe_Str__Fast_API__Route__Tag]`
14+
- **Impact areas**:
15+
- Route extraction needs to capture all tags from FastAPI
16+
- Contract extraction might need to handle tag-based organization differently
17+
- Client generation may need to consider how to organize methods when routes have multiple tags
18+
19+
### 3. Cascading Effects
20+
The tags change will likely affect:
21+
- `Fast_API__Route__Extractor._create_api_route()` - needs to extract all tags, not just first
22+
- Any code that assumes a single tag for grouping/organization
23+
- The module organization logic that might use tags for categorization

osbot_fast_api/client/Fast_API__Contract__Extractor.py

Lines changed: 99 additions & 129 deletions
Large diffs are not rendered by default.

osbot_fast_api/client/Fast_API__Route__Extractor.py

Lines changed: 67 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,125 @@
1-
from typing import List
1+
from typing import List, Union
2+
from osbot_utils.type_safe.type_safe_core.collections.Type_Safe__List import Type_Safe__List
3+
from osbot_fast_api.schemas.consts__Fast_API import FAST_API_DEFAULT_ROUTES_PATHS
24
from osbot_utils.type_safe.Type_Safe import Type_Safe
35
from fastapi import FastAPI
4-
from fastapi.routing import APIWebSocketRoute
6+
from fastapi.routing import APIWebSocketRoute, APIRoute, APIRouter
57
from osbot_utils.type_safe.primitives.domains.identifiers.safe_str.Safe_Str__Id import Safe_Str__Id
68
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
79
from starlette.middleware.wsgi import WSGIMiddleware
8-
from starlette.routing import Mount
10+
from starlette.routing import Mount, Route
911
from starlette.staticfiles import StaticFiles
1012
from osbot_fast_api.schemas.Safe_Str__Fast_API__Route__Prefix import Safe_Str__Fast_API__Route__Prefix
1113
from osbot_fast_api.schemas.for_osbot_utils.enums.Enum__Http__Method import Enum__Http__Method
1214
from osbot_fast_api.schemas.routes.Schema__Fast_API__Route import Schema__Fast_API__Route
1315
from osbot_fast_api.schemas.routes.Schema__Fast_API__Routes__Collection import Schema__Fast_API__Routes__Collection
1416
from osbot_fast_api.schemas.routes.enums.Enum__Route__Type import Enum__Route__Type
1517

16-
1718
class Fast_API__Route__Extractor(Type_Safe): # Dedicated class for route extraction
1819
app : FastAPI
1920
include_default : bool = False
2021
expand_mounts : bool = False
2122

2223
@type_safe
2324
def extract_routes(self) -> Schema__Fast_API__Routes__Collection: # Main extraction method
24-
routes = self.extract_routes_from_router(router = self.app ,
25+
routes = self.extract_routes_from_router(router = self.app.router ,
2526
route_prefix = Safe_Str__Fast_API__Route__Prefix('/'))
2627

2728
return Schema__Fast_API__Routes__Collection(routes = routes,
2829
total_routes = len(routes),
2930
has_mounts = any(r.is_mount for r in routes),
3031
has_websockets = any(r.route_type == Enum__Route__Type.WEBSOCKET for r in routes))
3132

32-
def extract_routes_from_router(self, router , # Router to extract from
33+
@type_safe
34+
def extract_routes_from_router(self, router : APIRouter , # FastAPI to extract routes from
3335
route_prefix : Safe_Str__Fast_API__Route__Prefix
3436
) -> List[Schema__Fast_API__Route]: # Returns list of route schemas
3537
routes = []
3638

3739
for route in router.routes: # Skip default routes if requested
38-
if not self.include_default and self._is_default_route(route.path):
40+
if not self.include_default and self.is_default_route(route.path):
3941
continue
4042

41-
# Build safe route path
42-
full_path = self._combine_paths(route_prefix, route.path)
43+
full_path = self._combine_paths(route_prefix, route.path) # Build safe route path
4344

44-
# Extract based on route type
45-
if isinstance(route, Mount):
46-
mount_routes = self._extract_mount_routes(route, full_path)
45+
if isinstance(route, Mount): # Extract based on route type
46+
mount_routes = self.extract_mount_routes(route, full_path)
4747
routes.extend(mount_routes)
4848
elif isinstance(route, APIWebSocketRoute):
49-
websocket_route = self._create_websocket_route(route, full_path)
49+
websocket_route = self.create_websocket_route(route, full_path)
5050
routes.append(websocket_route)
5151
else:
52-
api_route = self._create_api_route(route, full_path)
52+
api_route = self.create_api_route(route, full_path)
5353
routes.append(api_route)
5454

5555
return routes
5656

57-
def _create_api_route(self, route , # FastAPI route object
57+
@type_safe
58+
def create_api_route(self, route : Union[APIRoute, Route] , # FastAPI route object
5859
path : Safe_Str__Fast_API__Route__Prefix
59-
) -> Schema__Fast_API__Route: # Returns route schema
60-
# Convert methods to enum
61-
http_methods = []
62-
if hasattr(route, 'methods') and route.methods:
63-
for method in sorted(route.methods):
64-
try:
65-
http_methods.append(Enum__Http__Method(method))
66-
except ValueError:
67-
pass # Skip unknown methods
68-
69-
# Extract method name safely
70-
method_name = Safe_Str__Id(route.name) if route.name else Safe_Str__Id("unnamed")
71-
72-
# Determine route class if from Routes__* pattern
73-
route_class = self.extract_route_class(route)
74-
75-
return Schema__Fast_API__Route(http_path = path ,
76-
method_name = method_name ,
77-
http_methods = http_methods ,
78-
route_type = Enum__Route__Type.API_ROUTE ,
79-
route_class = route_class ,
80-
is_default = self._is_default_route(str(path) ))
81-
82-
def _extract_mount_routes(self, mount , # Mount object
60+
) -> Schema__Fast_API__Route: # Returns route schema
61+
http_methods = [] # Convert methods to enum
62+
for method in sorted(route.methods):
63+
http_methods.append(Enum__Http__Method(method))
64+
method_name = Safe_Str__Id(route.name)
65+
route_class = self.extract_route_class(route) # Determine route class if from Routes__* pattern
66+
if type(route_class) is APIRoute: # only the APIRoute class has the
67+
route_tags = route.tags # .tags method
68+
else:
69+
route_tags = None
70+
return Schema__Fast_API__Route(http_path = path ,
71+
method_name = method_name ,
72+
http_methods = http_methods ,
73+
route_type = Enum__Route__Type.API_ROUTE ,
74+
route_tags = route_tags ,
75+
route_class = route_class ,
76+
is_default = self.is_default_route(str(path)))
77+
78+
@type_safe
79+
def extract_mount_routes(self, mount: Mount , # Mount object
8380
path : Safe_Str__Fast_API__Route__Prefix
84-
) -> List[Schema__Fast_API__Route]: # Returns route schemas
85-
routes = []
81+
) -> List[Schema__Fast_API__Route]: # Returns route schemas
82+
routes = Type_Safe__List(expected_type=Schema__Fast_API__Route)
8683

8784
# Determine mount type
8885
if isinstance(mount.app, WSGIMiddleware):
89-
route = Schema__Fast_API__Route(
90-
http_path = path,
91-
method_name = Safe_Str__Id("wsgi_app"),
92-
http_methods = [], # Unknown methods for WSGI
93-
route_type = Enum__Route__Type.WSGI,
94-
is_mount = True
95-
)
86+
route = Schema__Fast_API__Route(http_path = path ,
87+
method_name = Safe_Str__Id("wsgi_app"),
88+
http_methods = [] , # Unknown methods for WSGI
89+
route_type = Enum__Route__Type.WSGI ,
90+
is_mount = True )
9691
routes.append(route)
9792

9893
elif isinstance(mount.app, StaticFiles):
99-
route = Schema__Fast_API__Route(
100-
http_path = path,
101-
method_name = Safe_Str__Id("static_files"),
102-
http_methods = [Enum__Http__Method.GET, Enum__Http__Method.HEAD],
103-
route_type = Enum__Route__Type.STATIC,
104-
is_mount = True
105-
)
94+
route = Schema__Fast_API__Route(http_path = path ,
95+
method_name = Safe_Str__Id("static_files") ,
96+
http_methods = [Enum__Http__Method.GET, Enum__Http__Method.HEAD],
97+
route_type = Enum__Route__Type.STATIC ,
98+
is_mount = True )
10699
routes.append(route)
107100

108-
elif self.expand_mounts and hasattr(mount.app, 'router'):
109-
# Recursively extract routes from mounted app
110-
mount_routes = self.extract_routes_from_router(
111-
router = mount.app.router,
112-
route_prefix = path
113-
)
101+
elif self.expand_mounts and hasattr(mount.app, 'router'): # Recursively extract routes from mounted app
102+
mount_routes = self.extract_routes_from_router(router = mount.app.router,
103+
route_prefix = path)
114104
routes.extend(mount_routes)
115-
else:
116-
# Generic mount
117-
route = Schema__Fast_API__Route(
118-
http_path = path,
119-
method_name = Safe_Str__Id("mount"),
120-
http_methods = [],
121-
route_type = Enum__Route__Type.MOUNT,
122-
is_mount = True
123-
)
105+
else: # Generic mount
106+
route = Schema__Fast_API__Route(http_path = path ,
107+
method_name = Safe_Str__Id("mount") ,
108+
http_methods = [] ,
109+
route_type = Enum__Route__Type.MOUNT ,
110+
is_mount = True )
124111
routes.append(route)
125112

126113
return routes
127114

128-
def _create_websocket_route(self, route , # WebSocket route
129-
path : Safe_Str__Fast_API__Route__Prefix
130-
) -> Schema__Fast_API__Route: # Returns route schema
131-
return Schema__Fast_API__Route(
132-
http_path = path,
133-
method_name = Safe_Str__Id(route.name) if route.name else Safe_Str__Id("websocket"),
134-
http_methods = [], # WebSockets don't use HTTP methods
135-
route_type = Enum__Route__Type.WEBSOCKET
136-
)
115+
@type_safe
116+
def create_websocket_route(self, route : APIWebSocketRoute , # WebSocket route
117+
path : Safe_Str__Fast_API__Route__Prefix
118+
) -> Schema__Fast_API__Route: # Returns route schema
119+
return Schema__Fast_API__Route(http_path = path ,
120+
method_name = Safe_Str__Id(route.name) , # if route.name else Safe_Str__Id("websocket"),
121+
http_methods = [] , # WebSockets don't use HTTP methods
122+
route_type = Enum__Route__Type.WEBSOCKET)
137123

138124
def _combine_paths(self, prefix : Safe_Str__Fast_API__Route__Prefix, # Prefix path
139125
path : str # Path to append
@@ -149,12 +135,10 @@ def _combine_paths(self, prefix : Safe_Str__Fast_API__Route__Prefix, # Prefix
149135

150136
return Safe_Str__Fast_API__Route__Prefix(combined)
151137

152-
def _is_default_route(self, path: str) -> bool: # Check if default route
153-
# Import here to avoid circular dependency
154-
from osbot_fast_api.schemas.consts__Fast_API import FAST_API_DEFAULT_ROUTES_PATHS
138+
def is_default_route(self, path: str) -> bool: # Check if default route
155139
return path in FAST_API_DEFAULT_ROUTES_PATHS
156140

157-
def extract_route_class(self, route) -> Safe_Str__Id: # Extract class name (in most cases it will be something like Routes__* )
141+
def extract_route_class(self, route) -> Safe_Str__Id: # Extract class name (in most cases it will be something like Routes__* )
158142
route_class = None
159143
if hasattr(route, 'endpoint'):
160144
if hasattr(route.endpoint, '__self__'): # first try to get the class name (if inside a class)

osbot_fast_api/client/schemas/Schema__Endpoint__Contract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Schema__Endpoint__Contract(Type_Safe):
1212

1313
# Endpoint identity - what FastAPI tells us
1414
operation_id : Safe_Str__Id # Unique operation identifier
15-
method : Enum__Http__Method # HTTP method
15+
method : Enum__Http__Method # Single HTTP method for this contract
1616
path_pattern : Safe_Str__Fast_API__Route__Prefix # URL pattern with {params}
1717

1818
# Route class mapping - how the service organizes code

osbot_fast_api/client/testing/Test__Fast_API__With_Routes.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,12 @@ class Schema__Product(Type_Safe):
3333
class Routes__Users(Fast_API__Routes): # Test route class - Users
3434
tag = 'users'
3535

36-
def get_user__user_id(self, user_id: int) -> Schema__User:
37-
"""Get user by ID"""
36+
def get_user__user_id(self, user_id: int) -> Schema__User: # Get user by ID
3837
if user_id == 0:
3938
raise HTTPException(status_code=404, detail="User not found")
4039
return Schema__User(id=user_id, name="Test User", email="test@test.com")
4140

42-
def create_user(self, user: Schema__User) -> Schema__User:
43-
"""Create new user"""
41+
def create_user(self, user: Schema__User) -> Schema__User: # Create new user
4442
return user
4543

4644
def setup_routes(self):
@@ -51,16 +49,13 @@ def setup_routes(self):
5149
class Routes__Products(Fast_API__Routes): # Test route class - Products
5250
tag = 'products'
5351

54-
def get_product__product_id(self, product_id: int) -> Schema__Product:
55-
"""Get product by ID"""
52+
def get_product__product_id(self, product_id: int) -> Schema__Product: # Get product by ID
5653
return Schema__Product(id=product_id, name="Test Product", price=99.99)
5754

58-
def list_products(self, limit: int = 10, offset: int = 0) -> list:
59-
"""List products with pagination"""
55+
def list_products(self, limit: int = 10, offset: int = 0) -> list: # List products with pagination
6056
return []
6157

62-
def update_product__product_id(self, product_id: int, product: Schema__Product) -> Schema__Product:
63-
"""Update product"""
58+
def update_product__product_id(self, product_id: int, product: Schema__Product) -> Schema__Product: # Update product
6459
return product
6560

6661
def setup_routes(self):

0 commit comments

Comments
 (0)