1+ # ==============================================================================
2+ # WARNING: This module currently violates the Single Responsibility Principle.
3+ # It takes on multiple concerns that should ideally be decoupled:
4+ #
5+ # Responsibilities overloaded:
6+ # - Route registration
7+ # - OpenAPI schema generation
8+ # - Parameter resolution and validation
9+ # - Response serialization
10+ # - Error handling
11+ # - API documentation UI rendering
12+ #
13+ # Structural issues:
14+ # - Methods operate at inconsistent levels of abstraction
15+ # - Some functions are overly long and do too much
16+ # - Inter-method cohesion is weak or unclear
17+ #
18+ # Risks and pitfalls:
19+ # - Missing or weak type annotations in places
20+ # - Logic and presentation are mixed in some implementations
21+ #
22+ # I'm actively collecting feedback and tracking bugs.
23+ # A full refactor is planned once this phase is complete.
24+ # ==============================================================================
25+
126import inspect
227import re
328import typing
4- import warnings
529from collections .abc import Callable
630from http import HTTPStatus
731from typing import Any , ClassVar
@@ -44,7 +68,6 @@ class BaseRouter:
4468 - title: Title of the API documentation (defaults to "My App").
4569 - version: Version of the API (defaults to "0.1.0").
4670 - description: Description of the API
47- - use_aliases: Temporary argument to maintain backward compatibility
4871 (included in OpenAPI info, default "API documentation").
4972
5073 The BaseRouter allows defining routes using decorator methods (get, post, etc.).
@@ -65,7 +88,6 @@ def __init__(
6588 title : str = "My App" ,
6689 version : str = "0.1.0" ,
6790 description : str = "API documentation" ,
68- use_aliases : bool = True ,
6991 ):
7092 self .app = app
7193 self .docs_url = docs_url
@@ -77,15 +99,6 @@ def __init__(
7799 self .description = description
78100 self ._routes : list [tuple [str , str , Callable ]] = []
79101 self ._openapi_schema = None
80- self .use_aliases = use_aliases
81- # TODO Remove use_aliases in 0.7.0
82- if not use_aliases :
83- warnings .warn (
84- "Setting use_aliases=False is deprecated. "
85- "It will be removed in version 0.7.0" ,
86- FutureWarning ,
87- stacklevel = 2 ,
88- )
89102 if self .app is not None :
90103 if self .docs_url and self .redoc_url and self .openapi_url :
91104 self ._register_docs_endpoints ()
@@ -226,53 +239,85 @@ def _build_operation(
226239 def _build_parameters_and_body (
227240 self , endpoint , definitions : dict , route_path : str , http_method : str
228241 ):
242+ """Build OpenAPI parameters and request body from endpoint signature."""
229243 sig = inspect .signature (endpoint )
230244 parameters = []
231245 request_body = None
232-
233246 path_params = {match .group (1 ) for match in re .finditer (r"{(\w+)}" , route_path )}
234247
235248 for param_name , param in sig .parameters .items ():
236- if isinstance (param .annotation , type ) and issubclass (
237- param .annotation , BaseModel
238- ):
249+ if self ._is_pydantic_model (param .annotation ):
239250 if http_method .upper () == "GET" :
240- # TODO Remove use_aliases in 0.7.0
241- model_schema = param .annotation .model_json_schema (
242- mode = "serialization" if self .use_aliases else "validation"
243- )
244- required_fields = model_schema .get ("required" , [])
245- properties = model_schema .get ("properties" , {})
246- for prop_name , prop_schema in properties .items ():
247- parameters .append (
248- {
249- "name" : prop_name ,
250- "in" : "query" ,
251- "required" : prop_name in required_fields ,
252- "schema" : prop_schema ,
253- }
254- )
251+ query_params = self ._build_query_params_from_model (param .annotation )
252+ parameters .extend (query_params )
255253 else :
256254 model_schema = self ._get_model_schema (param .annotation , definitions )
257255 request_body = {
258256 "content" : {"application/json" : {"schema" : model_schema }},
259257 "required" : param .default is inspect .Parameter .empty ,
260258 }
261259 else :
262- location = "path" if param_name in path_params else "query"
263- openapi_type = PYTHON_TYPE_MAPPING .get (param .annotation , "string" )
264- parameters .append (
265- {
266- "name" : param_name ,
267- "in" : location ,
268- "required" : (param .default is inspect .Parameter .empty )
269- or (location == "path" ),
270- "schema" : {"type" : openapi_type },
271- }
272- )
260+ param_info = self ._build_parameter_info (param_name , param , path_params )
261+ parameters .append (param_info )
273262
274263 return parameters , request_body
275264
265+ @staticmethod
266+ def _is_pydantic_model (annotation ) -> bool :
267+ return isinstance (annotation , type ) and issubclass (annotation , BaseModel )
268+
269+ @staticmethod
270+ def _build_query_params_from_model (model_class ) -> list :
271+ """Convert Pydantic model fields to query parameters."""
272+ parameters = []
273+ model_schema = model_class .model_json_schema (mode = "serialization" )
274+ required_fields = model_schema .get ("required" , [])
275+ properties = model_schema .get ("properties" , {})
276+
277+ for prop_name , prop_schema in properties .items ():
278+ parameters .append (
279+ {
280+ "name" : prop_name ,
281+ "in" : "query" ,
282+ "required" : prop_name in required_fields ,
283+ "schema" : prop_schema ,
284+ }
285+ )
286+
287+ return parameters
288+
289+ def _build_parameter_info (self , param_name , param , path_params ) -> dict :
290+ """Build parameter info for path or query parameters."""
291+ location = "path" if param_name in path_params else "query"
292+ schema = self ._build_parameter_schema (param .annotation )
293+ is_required = param .default is inspect .Parameter .empty or location == "path"
294+
295+ return {
296+ "name" : param_name ,
297+ "in" : location ,
298+ "required" : is_required ,
299+ "schema" : schema ,
300+ }
301+
302+ def _build_parameter_schema (self , annotation ) -> dict :
303+ """Build OpenAPI schema for a parameter based on its type annotation."""
304+ origin = typing .get_origin (annotation )
305+ if origin is list :
306+ return self ._build_array_schema (annotation )
307+
308+ return {"type" : PYTHON_TYPE_MAPPING .get (annotation , "string" )}
309+
310+ def _build_array_schema (self , list_annotation ) -> dict :
311+ """Build OpenAPI schema for an array type."""
312+ args = typing .get_args (list_annotation )
313+ item_type = "string" # default
314+
315+ # Get the inner type if available
316+ if args and args [0 ] in PYTHON_TYPE_MAPPING :
317+ item_type = PYTHON_TYPE_MAPPING [args [0 ]]
318+
319+ return {"type" : "array" , "items" : {"type" : item_type }}
320+
276321 def _build_responses (self , meta : dict , definitions : dict , status_code : str ) -> dict :
277322 responses = {status_code : {"description" : HTTPStatus (int (status_code )).phrase }}
278323 response_model = meta .get ("response_model" )
@@ -347,31 +392,29 @@ def _register_docs_endpoints(self):
347392 """
348393 raise NotImplementedError
349394
350- def _serialize_response (self , result : Any ) -> Any :
351- from pydantic import BaseModel
352-
395+ @classmethod
396+ def _serialize_response (cls , result : Any ) -> Any :
353397 if isinstance (result , BaseModel ):
354- # TODO Remove use_aliases in 0.7.0
355- return result .model_dump (by_alias = self .use_aliases )
398+ return result .model_dump (by_alias = True )
356399 if isinstance (result , list ):
357- return [self ._serialize_response (item ) for item in result ]
400+ return [cls ._serialize_response (item ) for item in result ]
358401 if isinstance (result , dict ):
359- return {k : self ._serialize_response (v ) for k , v in result .items ()}
402+ return {k : cls ._serialize_response (v ) for k , v in result .items ()}
360403 return result
361404
362- def _get_model_schema (self , model : type [BaseModel ], definitions : dict ) -> dict :
405+ @classmethod
406+ def _get_model_schema (cls , model : type [BaseModel ], definitions : dict ) -> dict :
363407 """
364408 Get the OpenAPI schema for a Pydantic model, with caching for better performance
365409 """
366410 model_name = model .__name__
367411 cache_key = f"{ model .__module__ } .{ model_name } "
368412
369413 # Check if the schema is already in the class-level cache
370- if cache_key not in self ._model_schema_cache :
414+ if cache_key not in cls ._model_schema_cache :
371415 # Generate the schema if it's not in the cache
372- # TODO Remove use_aliases in 0.7.0
373416 model_schema = model .model_json_schema (
374- mode = "serialization" if self . use_aliases else "validation" ,
417+ mode = "serialization" ,
375418 ref_template = "#/components/schemas/{model}" ,
376419 )
377420
@@ -382,11 +425,11 @@ def _get_model_schema(self, model: type[BaseModel], definitions: dict) -> dict:
382425 del model_schema [key ]
383426
384427 # Add schema to the cache
385- self ._model_schema_cache [cache_key ] = model_schema
428+ cls ._model_schema_cache [cache_key ] = model_schema
386429
387430 # Make sure the schema is in the definitions dictionary
388431 if model_name not in definitions :
389- definitions [model_name ] = self ._model_schema_cache [cache_key ]
432+ definitions [model_name ] = cls ._model_schema_cache [cache_key ]
390433
391434 return {"$ref" : f"#/components/schemas/{ model_name } " }
392435
0 commit comments