@ -2,17 +2,17 @@
Pydantic schemas for API requests and responses
"""
from datetime import datetime , date
from datetime import datetime , date , timezone
from typing import Optional , Dict , List , Any
from pydantic import BaseModel , Field , ConfigDict , validator
from pydantic import BaseModel , Field , ConfigDict , field_ validator
from enum import Enum
import uuid
import re
from . validators import (
validate_period_field ,
validate_quarters_field ,
validate_time_approaches ,
validate_end_date_field
validate_end_date_field ,
validate_interval_field ,
)
class PeriodType ( str , Enum ) :
@ -74,21 +74,25 @@ class FinancialDataRequest(BaseModel):
include_metrics : bool = Field ( True , description = " Include calculated metrics in response " )
force_refresh : bool = Field ( False , description = " Force refresh data from SEC " )
@validator ( ' period ' )
@field_validator ( ' period ' )
@classmethod
def validate_period ( cls , v ) :
return validate_period_field ( cls , v )
@validator ( ' quarters ' )
@field_validator ( ' quarters ' )
@classmethod
def validate_quarters ( cls , v ) :
return validate_quarters_field ( cls , v )
@validator ( ' start_date ' )
def validate_time_approaches ( cls , v , values ) :
return validate_time_approaches ( cls , v , values )
@field_validator ( ' start_date ' )
@classmethod
def validate_time_approaches ( cls , v , info ) :
return validate_time_approaches ( cls , v , info . data )
@validator ( ' end_date ' )
def validate_end_date ( cls , v , values ) :
return validate_end_date_field ( cls , v , values )
@field_validator ( ' end_date ' )
@classmethod
def validate_end_date ( cls , v , info ) :
return validate_end_date_field ( cls , v , info . data )
class BulkFinancialDataRequest ( BaseModel ) :
tickers : List [ str ] = Field ( . . . , min_items = 1 , max_items = 500 , description = " List of stock ticker symbols (max 500 for efficient bulk processing) " )
@ -115,30 +119,28 @@ class BulkFinancialDataRequest(BaseModel):
include_metrics : bool = Field ( True , description = " Include calculated metrics in response " )
force_refresh : bool = Field ( False , description = " Force refresh data from SEC " )
@validator ( ' quarters ' )
@field_validator ( ' quarters ' )
@classmethod
def validate_quarters ( cls , v ) :
""" Validate quarter format """
if v :
for quarter in v :
if not re . match ( r ' ^ \ d {4} Q[1-4]$ ' , quarter ) :
raise ValueError ( f " Invalid quarter format: { quarter } . Expected format: YYYYQN (e.g., 2020Q1) " )
return v
return validate_quarters_field ( cls , v )
@validator ( ' start_date ' )
def validate_dates_or_quarters ( cls , v , values ) :
@field_validator ( ' start_date ' )
@classmethod
def validate_dates_or_quarters ( cls , v , info ) :
""" Ensure either dates or quarters are provided """
quarters = values . get ( ' quarters ' )
quarters = info. data . get ( ' quarters ' )
if not v and not quarters :
raise ValueError ( " Either start_date/end_date or quarters must be provided " )
if v and quarters :
raise ValueError ( " Cannot specify both date range and quarters - use one or the other " )
return v
@validator ( ' end_date ' )
def validate_end_date ( cls , v , values ) :
@field_validator ( ' end_date ' )
@classmethod
def validate_end_date ( cls , v , info ) :
""" Validate end_date if using date-based approach """
start_date = values . get ( ' start_date ' )
quarters = values . get ( ' quarters ' )
start_date = info. data . get ( ' start_date ' )
quarters = info. data . get ( ' quarters ' )
if not quarters : # Using date-based approach
if not v :
@ -182,38 +184,33 @@ class PriceDataRequest(BaseModel):
interval : str = Field ( " 1d " , description = " Data interval: 1d, 1w, 1m, 5d, 1h, etc. " )
force_refresh : bool = Field ( False , description = " Force refresh data from Yahoo Finance " )
@validator ( ' interval ' )
@field_validator ( ' interval ' )
@classmethod
def validate_interval ( cls , v ) :
""" Validate interval format """
valid_intervals = [ ' 1m ' , ' 2m ' , ' 5m ' , ' 15m ' , ' 30m ' , ' 60m ' , ' 90m ' , ' 1h ' , ' 1d ' , ' 5d ' , ' 1w ' , ' 1mo ' , ' 3mo ' ]
if v not in valid_intervals :
raise ValueError ( f " Invalid interval: { v } . Valid intervals: { ' , ' . join ( valid_intervals ) } " )
return v
return validate_interval_field ( cls , v )
@validator ( ' quarters ' )
@field_validator ( ' quarters ' )
@classmethod
def validate_quarters ( cls , v ) :
""" Validate quarter format """
if v :
for quarter in v :
if not re . match ( r ' ^ \ d {4} Q[1-4]$ ' , quarter ) :
raise ValueError ( f " Invalid quarter format: { quarter } . Expected format: YYYYQN (e.g., 2020Q1) " )
return v
return validate_quarters_field ( cls , v )
@validator ( ' start_date ' )
def validate_dates_or_quarters ( cls , v , values ) :
@field_validator ( ' start_date ' )
@classmethod
def validate_dates_or_quarters ( cls , v , info ) :
""" Ensure either dates or quarters are provided """
quarters = values . get ( ' quarters ' )
quarters = info. data . get ( ' quarters ' )
if not v and not quarters :
raise ValueError ( " Either start_date/end_date or quarters must be provided " )
if v and quarters :
raise ValueError ( " Cannot specify both date range and quarters - use one or the other " )
return v
@validator ( ' end_date ' )
def validate_end_date ( cls , v , values ) :
@field_validator ( ' end_date ' )
@classmethod
def validate_end_date ( cls , v , info ) :
""" Validate end_date if using date-based approach """
start_date = values . get ( ' start_date ' )
quarters = values . get ( ' quarters ' )
start_date = info. data . get ( ' start_date ' )
quarters = info. data . get ( ' quarters ' )
if not quarters : # Using date-based approach
if not v :
@ -246,38 +243,33 @@ class BulkPriceDataRequest(BaseModel):
interval : str = Field ( " 1d " , description = " Data interval: 1d, 1w, 1m, 5d, 1h, etc. " )
force_refresh : bool = Field ( False , description = " Force refresh data from Yahoo Finance " )
@validator ( ' interval ' )
@field_validator ( ' interval ' )
@classmethod
def validate_interval ( cls , v ) :
""" Validate interval format """
valid_intervals = [ ' 1m ' , ' 2m ' , ' 5m ' , ' 15m ' , ' 30m ' , ' 60m ' , ' 90m ' , ' 1h ' , ' 1d ' , ' 5d ' , ' 1w ' , ' 1mo ' , ' 3mo ' ]
if v not in valid_intervals :
raise ValueError ( f " Invalid interval: { v } . Valid intervals: { ' , ' . join ( valid_intervals ) } " )
return v
return validate_interval_field ( cls , v )
@validator ( ' quarters ' )
@field_validator ( ' quarters ' )
@classmethod
def validate_quarters ( cls , v ) :
""" Validate quarter format """
if v :
for quarter in v :
if not re . match ( r ' ^ \ d {4} Q[1-4]$ ' , quarter ) :
raise ValueError ( f " Invalid quarter format: { quarter } . Expected format: YYYYQN (e.g., 2020Q1) " )
return v
return validate_quarters_field ( cls , v )
@validator ( ' start_date ' )
def validate_dates_or_quarters ( cls , v , values ) :
@field_validator ( ' start_date ' )
@classmethod
def validate_dates_or_quarters ( cls , v , info ) :
""" Ensure either dates or quarters are provided """
quarters = values . get ( ' quarters ' )
quarters = info. data . get ( ' quarters ' )
if not v and not quarters :
raise ValueError ( " Either start_date/end_date or quarters must be provided " )
if v and quarters :
raise ValueError ( " Cannot specify both date range and quarters - use one or the other " )
return v
@validator ( ' end_date ' )
def validate_end_date ( cls , v , values ) :
@field_validator ( ' end_date ' )
@classmethod
def validate_end_date ( cls , v , info ) :
""" Validate end_date if using date-based approach """
start_date = values . get ( ' start_date ' )
quarters = values . get ( ' quarters ' )
start_date = info. data . get ( ' start_date ' )
quarters = info. data . get ( ' quarters ' )
if not quarters : # Using date-based approach
if not v :
@ -384,7 +376,8 @@ class PriceDataPoint(BaseModel):
adjusted_close : Optional [ float ] = None
data_source : str
@validator ( ' date ' , pre = True )
@field_validator ( ' date ' , mode = ' before ' )
@classmethod
def convert_datetime_to_date ( cls , v ) :
""" Convert datetime to date if needed """
if isinstance ( v , datetime ) :
@ -405,7 +398,8 @@ class AlpacaPriceDataPoint(BaseModel):
trade_count : Optional [ int ] = None
data_source : str = " ALPACA "
@validator ( ' date ' , pre = True )
@field_validator ( ' date ' , mode = ' before ' )
@classmethod
def convert_datetime_to_date ( cls , v ) :
if isinstance ( v , datetime ) :
return v . date ( )
@ -448,7 +442,7 @@ class ErrorResponse(BaseModel):
error_type : ErrorType
message : str
detail : Optional [ Dict [ str , Any ] ] = None
timestamp : datetime = Field ( default_factory = datetime . utcnow )
timestamp : datetime = Field ( default_factory = lambda : datetime . now ( timezone . utc ) )
# New schemas for quote/intraday/today endpoints
@ -526,3 +520,32 @@ class MigrationResponse(BaseModel):
failed_records : int
errors : List [ Dict [ str , Any ] ] = Field ( default_factory = list )
duration_seconds : float
class AlpacaBarsResponse ( BaseModel ) :
ticker : str
interval : str
count : int
bars : List [ Dict [ str , Any ] ]
class AlpacaIntradayResponse ( BaseModel ) :
ticker : str
interval : str
source : str = " ALPACA "
count : int
candles : List [ Dict [ str , Any ] ]
class NewsOnlyResponse ( BaseModel ) :
ticker : str
retrieved_at : str
news : Dict [ str , Any ]
summary : Dict [ str , Any ]
class SocialOnlyResponse ( BaseModel ) :
ticker : str
retrieved_at : str
social_media : Dict [ str , Any ]
summary : Dict [ str , Any ]