Updating types

This commit is contained in:
James Ketr 2025-05-28 13:36:35 -07:00
parent 168de8a2b9
commit f7e41c710c
25 changed files with 187 additions and 556 deletions

View File

@ -4,12 +4,12 @@ import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from './BackstoryTheme';
import { SeverityType } from './components/Snack';
import { Query } from './types/types';
import { ConversationHandle } from './components/Conversation';
import { UserProvider } from './components/UserContext';
import { UserRoute } from './routes/UserRoute';
import { BackstoryLayout } from './components/BackstoryLayout';
import { SeverityType } from 'components/Snack';
import { Query } from 'types/types';
import { ConversationHandle } from 'components/Conversation';
import { UserProvider } from 'components/UserContext';
import { UserRoute } from 'routes/UserRoute';
import { BackstoryLayout } from 'components/layout/BackstoryLayout';
import './BackstoryApp.css';
import '@fontsource/roboto/300.css';
@ -17,7 +17,7 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { connectionBase } from './Global';
import { connectionBase } from './utils/Global';
// Cookie handling functions
const getCookie = (name: string) => {

112
frontend/src/README.md Normal file
View File

@ -0,0 +1,112 @@
# Disk structure
Below is the general directory structure for the Backstory platform, prioritizing maintainability and developer experience:
```
src/
├── components/ # Reusable UI components
│ ├── common/ # Generic components (Button, Modal, etc.)
│ ├── forms/ # Form-related components
│ ├── layout/ # Layout components (Header, Sidebar, etc.)
│ └── ui/ # MUI customizations and themed components
├── pages/ # Page-level components (route components)
│ ├── auth/
│ ├── dashboard/
│ ├── profile/
│ └── settings/
├── features/ # Feature-specific modules
│ ├── authentication/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
│ ├── user-management/
│ └── analytics/
├── hooks/ # Custom React hooks
│ ├── api/ # API-related hooks
│ ├── ui/ # UI state hooks
│ └── utils/ # Utility hooks
├── services/ # API calls and external services
│ ├── api/
│ ├── auth/
│ └── storage/
├── store/ # State management (Redux/Zustand/Context)
│ ├── slices/ # If using Redux Toolkit
│ ├── providers/ # Context providers
│ └── types/
├── utils/ # Pure utility functions
│ ├── constants/
│ ├── helpers/
│ └── validators/
├── styles/ # Global styles and theme
│ ├── theme/ # MUI theme customization
│ ├── globals.css
│ └── variables.css
├── types/ # TypeScript type definitions
│ ├── api/
│ ├── common/
│ └── components/
├── assets/ # Static assets
│ ├── images/
│ ├── icons/
│ └── fonts/
├── config/ # Configuration files
│ ├── env.ts
│ ├── routes.ts
│ └── constants.ts
└── __tests__/ # Test files mirroring src structure
├── components/
├── pages/
└── utils/
```
# Key organizational principles:
1. Feature-Based Architecture
The features/ directory groups related functionality together, making it easy to find everything related to a specific feature in one place.
2. Clear Separation of Concerns
```
components/ - Pure UI components
pages/ - Route-level components
services/ - Data fetching and external APIs
hooks/ - Reusable logic
utils/ - Pure functions
```
3. Scalable Component Organization
Components are organized by purpose rather than alphabetically, with subcategories that make sense as the app grows.
4. Centralized Configuration
All app configuration lives in config/, making it easy to manage environment variables, routes, and constants.
5. Type Safety First
Dedicated types/ directory with clear categorization helps maintain type definitions as the app scales.
# Naming Conventions
* Use PascalCase for components (UserProfile.tsx)
* Use camelCase for utilities and hooks (formatDate.ts, useLocalStorage.ts)
* Use kebab-case for directories (user-management/)
# Index Files
Create index.ts files in major directories to enable clean imports:
```typescript
// components/common/index.ts
export { Button } from './Button';
export { Modal } from './Modal';
// Import usage
import { Button, Modal } from '@/components/common';
```
# Path Aliases
Configure path aliases in your build tool:
```typescript
// Instead of: ../../../../components/common/Button
import { Button } from '@/components/common/Button';
```
This structure scales well while keeping related code co-located and maintaining clear boundaries between different types of functionality.

View File

@ -24,6 +24,8 @@ import PhoneInput from 'react-phone-number-input';
import { E164Number } from 'libphonenumber-js/core';
import './PhoneInput.css';
import { ApiClient } from 'types/api-client';
// Import conversion utilities
import {
formatApiRequest,
@ -36,9 +38,8 @@ import {
} from './types/conversion';
import {
AuthResponse, BaseUser, Guest
AuthResponse, BaseUser, Guest, Candidate
} from './types/types'
import { connectionBase } from 'Global';
interface LoginRequest {
login: string;
@ -54,9 +55,8 @@ interface RegisterRequest {
phone?: string;
}
const API_BASE_URL = `${connectionBase}/api/1.0`;
const BackstoryTestApp: React.FC = () => {
const apiClient = new ApiClient();
const [currentUser, setCurrentUser] = useState<BaseUser | null>(null);
const [guestSession, setGuestSession] = useState<Guest | null>(null);
const [tabValue, setTabValue] = useState(0);
@ -139,24 +139,7 @@ const BackstoryTestApp: React.FC = () => {
setSuccess(null);
try {
// Format request data for API (camelCase to snake_case)
const requestData = formatApiRequest({
login: loginForm.login,
password: loginForm.password
});
debugConversion(requestData, 'Login Request');
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
// Use conversion utility to handle response
const authResponse = await handleApiResponse<AuthResponse>(response);
const authResponse = await apiClient.login(loginForm.login, loginForm.password)
debugConversion(authResponse, 'Login Response');
@ -186,7 +169,7 @@ const BackstoryTestApp: React.FC = () => {
setSuccess(null);
try {
const candidateData = {
const candidate: Candidate = {
username: registerForm.username,
email: registerForm.email,
firstName: registerForm.firstName,
@ -210,21 +193,7 @@ const BackstoryTestApp: React.FC = () => {
}
};
// Format request data for API (camelCase to snake_case, dates to ISO strings)
const requestData = formatApiRequest(candidateData);
debugConversion(requestData, 'Registration Request');
const response = await fetch(`${API_BASE_URL}/candidates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
// Use conversion utility to handle response
const result = await handleApiResponse<any>(response);
const result = await apiClient.createCandidate(candidate);
debugConversion(result, 'Registration Response');

View File

@ -9,13 +9,13 @@ import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader";
import { Message, MessageList, BackstoryMessage, MessageRoles } from './Message';
import { DeleteConfirmation } from './DeleteConfirmation';
import { Query } from '../types/types';
import { BackstoryTextField, BackstoryTextFieldRef } from './BackstoryTextField';
import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { Query } from 'types/types';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab';
import { connectionBase } from '../Global';
import { useUser } from "./UserContext";
import { streamQueryResponse, StreamQueryController } from './streamQueryResponse';
import { connectionBase } from 'utils/Global';
import { useUser } from "components/UserContext";
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
import './Conversation.css';

View File

@ -2,10 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
import Box from '@mui/material/Box';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { Quote } from 'components/Quote';
import { streamQueryResponse, StreamQueryController } from './streamQueryResponse';
import { connectionBase } from 'Global';
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
import { connectionBase } from 'utils/Global';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { useUser } from './UserContext';
import { useUser } from 'components/UserContext';
interface GenerateImageProps extends BackstoryElementProps {
prompt: string

View File

@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
import mermaid, { MermaidConfig } from 'mermaid';
import { SxProps } from '@mui/material/styles';
import { Box } from '@mui/material';
import { useResizeObserverAndMutationObserver } from './useAutoScrollToBottom';
import { useResizeObserverAndMutationObserver } from '../hooks/useAutoScrollToBottom';
const defaultMermaidConfig : MermaidConfig = {
startOnLoad: true,

View File

@ -76,7 +76,6 @@ type BackstoryMessage = {
expandable?: boolean,
};
interface ChatBubbleProps {
role: MessageRoles,
isInfo?: boolean;

View File

@ -1,7 +1,7 @@
import Box from '@mui/material/Box';
import { SxProps, Theme } from '@mui/material';
import { RefObject, useRef } from 'react';
import { useAutoScrollToBottom } from './useAutoScrollToBottom';
import { useAutoScrollToBottom } from '../hooks/useAutoScrollToBottom';
interface ScrollableProps {
children?: React.ReactNode;

View File

@ -1,6 +1,6 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import { SetSnackType } from './Snack';
import { connectionBase } from '../Global';
import { connectionBase } from '../utils/Global';
import { User } from '../types/types';
type UserContextType = {

View File

@ -17,7 +17,7 @@ import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import { Scrollable } from './Scrollable';
import { connectionBase } from '../Global';
import { connectionBase } from '../utils/Global';
import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab';

View File

@ -17,7 +17,7 @@ import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from 'components/Snack';
import { useUser } from 'components/UserContext';
import { User } from 'types/types';
import { getBackstoryDynamicRoutes } from 'components/BackstoryRoutes';
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
import { LoadingComponent } from "components/LoadingComponent";
type NavigationLinkType = {
@ -73,9 +73,9 @@ const getNavigationLinks = (user: User | null): NavigationLinkType[] => {
}
switch (user.userType) {
case 'UserType.CANDIDATE':
case 'candidate':
return CandidateNavItems;
case 'UserType.EMPLOYER':
case 'employer':
return EmployerNavItems;
default:
return DefaultNavItems;

View File

@ -2,8 +2,8 @@ import React, { Ref, ReactNode } from "react";
import { Route } from "react-router-dom";
import { Typography } from '@mui/material';
import { BackstoryPageProps } from './BackstoryTab';
import { ConversationHandle } from './Conversation';
import { BackstoryPageProps } from '../BackstoryTab';
import { ConversationHandle } from '../Conversation';
import { User } from 'types/types';
import { ChatPage } from 'pages/ChatPage';
@ -59,7 +59,7 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: Us
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
if (user.userType === "UserType.CANDIDATE") {
if (user.userType === 'candidate') {
routes.splice(-1, 0, ...[
<Route key={`${index++}`} path="/profile" element={<ProfilePage />} />,
<Route key={`${index++}`} path="/backstory" element={<BackstoryPage />} />,
@ -68,7 +68,7 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: Us
]);
}
if (user.userType === "UserType.EMPLOYER") {
if (user.userType === 'employer') {
routes.splice(-1, 0, ...[
<Route key={`${index++}`} path="/search" element={<SearchPage />} />,
<Route key={`${index++}`} path="/saved" element={<SavedPage />} />,

View File

@ -4,7 +4,7 @@ import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from './BackstoryTheme';
import { BrowserRouter as Router } from "react-router-dom";
import { BackstoryApp } from './BackstoryApp';
// import { BackstoryTestApp } from 'TestApp';
import { BackstoryTestApp } from 'TestApp';
import './index.css';
@ -16,8 +16,8 @@ root.render(
<React.StrictMode>
<ThemeProvider theme={backstoryTheme}>
<Router>
<BackstoryApp />
{/* <BackstoryTestApp /> */}
{/* <BackstoryApp /> */}
<BackstoryTestApp />
</Router>
</ThemeProvider>
</React.StrictMode>

View File

@ -5,7 +5,7 @@ import Box from '@mui/material/Box';
import { BackstoryPageProps } from '../components/BackstoryTab';
import { CandidateInfo } from 'components/CandidateInfo';
import { connectionBase } from '../Global';
import { connectionBase } from '../utils/Global';
import { Candidate } from "../types/types";
const CandidateListingPage = (props: BackstoryPageProps) => {

View File

@ -17,7 +17,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
const { user } = useUser();
const candidate: Candidate | null = (user && user.userType === "UserType.CANDIDATE") ? user as Candidate : null;
const candidate: Candidate | null = (user && user.userType === 'candidate') ? user as Candidate : null;
useEffect(() => {
if (!candidate) {

View File

@ -14,7 +14,7 @@ import Typography from '@mui/material/Typography';
// import ResetIcon from '@mui/icons-material/History';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { connectionBase } from '../Global';
import { connectionBase } from '../utils/Global';
import { BackstoryPageProps } from '../components/BackstoryTab';
interface ServerTunables {

View File

@ -8,15 +8,17 @@ import IconButton from '@mui/material/IconButton';
import CancelIcon from '@mui/icons-material/Cancel';
import SendIcon from '@mui/icons-material/Send';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { jsonrepair } from 'jsonrepair';
import { CandidateInfo } from '../components/CandidateInfo';
import { Query } from '../types/types'
import { Quote } from 'components/Quote';
import { streamQueryResponse, StreamQueryController } from '../components/streamQueryResponse';
import { connectionBase } from 'Global';
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
import { connectionBase } from 'utils/Global';
import { Candidate } from '../types/types';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { jsonrepair } from 'jsonrepair';
import { StyledMarkdown } from 'components/StyledMarkdown';
import { Scrollable } from '../components/Scrollable';
import { Pulse } from 'components/Pulse';

View File

@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { useUser } from "../components/UserContext";
import { User } from "../types/types";
import { Box } from "@mui/material";
import { connectionBase } from "../Global";
import { connectionBase } from "../utils/Global";
import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from "../components/LoadingComponent";

View File

@ -1,5 +1,5 @@
import { BackstoryMessage } from './Message';
import { Query } from '../types/types';
import { BackstoryMessage } from 'components/Message';
import { Query } from 'types/types';
import { jsonrepair } from 'jsonrepair';
type StreamQueryOptions = {

View File

@ -21,12 +21,17 @@ import {
PaginatedRequest
} from './conversion';
export class ApiClient {
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string, authToken?: string) {
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
constructor(authToken?: string) {
const loc = window.location;
if (!loc.host.match(/.*battle-linux.*/)) {
this.baseUrl = loc.protocol + "//" + loc.host + "/api/1.0";
} else {
this.baseUrl = loc.protocol + "//battle-linux.ketrenos.com:8912/api/1.0";
}
this.defaultHeaders = {
'Content-Type': 'application/json',
...(authToken && { 'Authorization': `Bearer ${authToken}` })
@ -367,13 +372,6 @@ export class ApiClient {
getBaseUrl(): string {
return this.baseUrl;
}
/**
* Update base URL
*/
setBaseUrl(url: string): void {
this.baseUrl = url.replace(/\/$/, '');
}
}
// ============================
@ -382,7 +380,7 @@ export class ApiClient {
/*
// Initialize API client
const apiClient = new ApiClient('https://api.yourjobplatform.com');
const apiClient = new ApiClient();
// Login and set auth token
try {
@ -573,4 +571,4 @@ function CandidateList() {
}
*/
export default ApiClient;
export { ApiClient };

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// Source: src/models.py
// Generated on: 2025-05-27T23:44:38.806039
// Generated on: 2025-05-28T20:34:39.642452
// DO NOT EDIT MANUALLY - This file is auto-generated
// ============================
@ -181,7 +181,7 @@ export interface Candidate {
lastLogin?: Date;
profileImage?: string;
status: "active" | "inactive" | "pending" | "banned";
userType?: "UserType.CANDIDATE";
userType?: "candidate";
firstName: string;
lastName: string;
fullName: string;
@ -328,7 +328,7 @@ export interface Employer {
lastLogin?: Date;
profileImage?: string;
status: "active" | "inactive" | "pending" | "banned";
userType?: "UserType.EMPLOYER";
userType?: "employer";
companyName: string;
industry: string;
description?: string;

View File

@ -114,10 +114,18 @@ def python_type_to_typescript(python_type: Any) -> str:
return f"Record<{key_type}, {value_type}>"
return "Record<string, any>"
# Handle Literal types
# Handle Literal types - UPDATED SECTION
if hasattr(python_type, '__origin__') and str(python_type.__origin__).endswith('Literal'):
if args:
literal_values = [f'"{arg}"' if isinstance(arg, str) else str(arg) for arg in args]
literal_values = []
for arg in args:
if isinstance(arg, Enum):
# Handle enum values within literals
literal_values.append(f'"{arg.value}"')
elif isinstance(arg, str):
literal_values.append(f'"{arg}"')
else:
literal_values.append(str(arg))
return " | ".join(literal_values)
# Handle Enum types
@ -125,6 +133,10 @@ def python_type_to_typescript(python_type: Any) -> str:
enum_values = [f'"{v.value}"' for v in python_type]
return " | ".join(enum_values)
# Handle individual enum instances
if isinstance(python_type, Enum):
return f'"{python_type.value}"'
# Handle datetime
if python_type == datetime:
return "Date"

View File

@ -1,461 +0,0 @@
#!/usr/bin/env python
"""
Enhanced Type Generator - Generate TypeScript types from Pydantic models
Now with command line parameters, pre-test validation, and TypeScript compilation
"""
import sys
import os
import argparse
import subprocess
from typing import Any, Dict, List, Optional, Union, get_origin, get_args
from datetime import datetime
from enum import Enum
from pathlib import Path
def run_command(command: str, description: str, cwd: str | None = None) -> bool:
"""Run a command and return success status"""
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
cwd=cwd
)
if result.returncode == 0:
print(f"{description}")
return True
else:
print(f"{description} failed:")
if result.stderr.strip():
print(f" Error: {result.stderr.strip()}")
if result.stdout.strip():
print(f" Output: {result.stdout.strip()}")
return False
except Exception as e:
print(f"{description} failed with exception: {e}")
return False
def run_focused_test() -> bool:
"""Run the focused test to validate models before generating types"""
print("🧪 Running focused test to validate models...")
# Get the directory of the currently executing script
script_dir = os.path.dirname(os.path.abspath(__file__))
test_file_path = os.path.join(script_dir, "focused_test.py")
if not os.path.exists(test_file_path):
print("❌ focused_test.py not found - skipping model validation")
return False
return run_command(f"python {test_file_path}", "Model validation")
def check_typescript_available() -> bool:
"""Check if TypeScript compiler is available"""
return run_command("npx tsc --version", "TypeScript version check")
# Add current directory to Python path so we can import models
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir)
try:
from pydantic import BaseModel # type: ignore
except ImportError as e:
print(f"Error importing pydantic: {e}")
print("Make sure pydantic is installed: pip install pydantic")
sys.exit(1)
def python_type_to_typescript(python_type: Any) -> str:
"""Convert a Python type to TypeScript type string"""
# Handle None/null
if python_type is type(None):
return "null"
# Handle basic types
if python_type == str:
return "string"
elif python_type == int or python_type == float:
return "number"
elif python_type == bool:
return "boolean"
elif python_type == dict or python_type == Dict:
return "Record<string, any>"
elif python_type == list or python_type == List:
return "Array<any>"
# Handle typing generics
origin = get_origin(python_type)
args = get_args(python_type)
if origin is Union:
# Handle Optional (Union[T, None])
if len(args) == 2 and type(None) in args:
non_none_type = next(arg for arg in args if arg is not type(None))
return python_type_to_typescript(non_none_type)
# Handle other unions
union_types = [python_type_to_typescript(arg) for arg in args if arg is not type(None)]
return " | ".join(union_types)
elif origin is list or origin is List:
if args:
item_type = python_type_to_typescript(args[0])
return f"Array<{item_type}>"
return "Array<any>"
elif origin is dict or origin is Dict:
if len(args) == 2:
key_type = python_type_to_typescript(args[0])
value_type = python_type_to_typescript(args[1])
return f"Record<{key_type}, {value_type}>"
return "Record<string, any>"
# Handle Literal types
if hasattr(python_type, '__origin__') and str(python_type.__origin__).endswith('Literal'):
if args:
literal_values = [f'"{arg}"' if isinstance(arg, str) else str(arg) for arg in args]
return " | ".join(literal_values)
# Handle Enum types
if isinstance(python_type, type) and issubclass(python_type, Enum):
enum_values = [f'"{v.value}"' for v in python_type]
return " | ".join(enum_values)
# Handle datetime
if python_type == datetime:
return "Date"
# Handle Pydantic models
if isinstance(python_type, type) and issubclass(python_type, BaseModel):
return python_type.__name__
# Handle string representations
type_str = str(python_type)
if "EmailStr" in type_str:
return "string"
elif "HttpUrl" in type_str:
return "string"
elif "UUID" in type_str:
return "string"
# Default fallback
return "any"
def snake_to_camel(snake_str: str) -> str:
"""Convert snake_case to camelCase"""
components = snake_str.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
def process_pydantic_model(model_class) -> Dict[str, Any]:
"""Process a Pydantic model and return TypeScript interface definition"""
interface_name = model_class.__name__
properties = []
# Get fields from the model
if hasattr(model_class, 'model_fields'):
# Pydantic v2
fields = model_class.model_fields
for field_name, field_info in fields.items():
ts_name = snake_to_camel(field_name)
# Check for alias
if hasattr(field_info, 'alias') and field_info.alias:
ts_name = field_info.alias
# Get type annotation
field_type = getattr(field_info, 'annotation', str)
ts_type = python_type_to_typescript(field_type)
# Check if optional
is_optional = False
if hasattr(field_info, 'is_required'):
is_optional = not field_info.is_required()
elif hasattr(field_info, 'default'):
is_optional = field_info.default is not None
properties.append({
'name': ts_name,
'type': ts_type,
'optional': is_optional
})
elif hasattr(model_class, '__fields__'):
# Pydantic v1
fields = model_class.__fields__
for field_name, field_info in fields.items():
ts_name = snake_to_camel(field_name)
if hasattr(field_info, 'alias') and field_info.alias:
ts_name = field_info.alias
field_type = getattr(field_info, 'annotation', getattr(field_info, 'type_', str))
ts_type = python_type_to_typescript(field_type)
is_optional = not getattr(field_info, 'required', True)
if hasattr(field_info, 'default') and field_info.default is not None:
is_optional = True
properties.append({
'name': ts_name,
'type': ts_type,
'optional': is_optional
})
return {
'name': interface_name,
'properties': properties
}
def process_enum(enum_class) -> Dict[str, Any]:
"""Process an Enum and return TypeScript type definition"""
enum_name = enum_class.__name__
values = [f'"{v.value}"' for v in enum_class]
if len(values) == 0:
raise ValueError(f"Enum class '{enum_name}' has no values.")
return {
'name': enum_name,
'values': " | ".join(values)
}
def generate_typescript_interfaces(source_file: str):
"""Generate TypeScript interfaces from models"""
print(f"📖 Scanning {source_file} for Pydantic models and enums...")
# Import the models module dynamically
try:
import importlib.util
spec = importlib.util.spec_from_file_location("models", source_file)
if spec is None or spec.loader is None:
raise ImportError(f"Could not load module from {source_file}")
models_module = importlib.util.module_from_spec(spec)
sys.modules["models"] = models_module
spec.loader.exec_module(models_module)
except Exception as e:
print(f"❌ Error importing {source_file}: {e}")
return None
interfaces = []
enums = []
# Scan the models module
for name in dir(models_module):
obj = getattr(models_module, name)
# Skip private attributes
if name.startswith('_'):
continue
try:
# Check if it's a Pydantic model
if (isinstance(obj, type) and
issubclass(obj, BaseModel) and
obj != BaseModel):
interface = process_pydantic_model(obj)
interfaces.append(interface)
print(f" ✅ Found Pydantic model: {name}")
# Check if it's an Enum
elif (isinstance(obj, type) and
issubclass(obj, Enum)):
enum_def = process_enum(obj)
enums.append(enum_def)
print(f" ✅ Found enum: {name}")
except Exception as e:
print(f" ⚠️ Warning: Error processing {name}: {e}")
continue
print(f"\n📊 Found {len(interfaces)} interfaces and {len(enums)} enums")
# Generate TypeScript content
ts_content = f"""// Generated TypeScript types from Pydantic models
// Source: {source_file}
// Generated on: {datetime.now().isoformat()}
// DO NOT EDIT MANUALLY - This file is auto-generated
"""
# Add enums
if enums:
ts_content += "// ============================\n"
ts_content += "// Enums\n"
ts_content += "// ============================\n\n"
for enum_def in enums:
ts_content += f"export type {enum_def['name']} = {enum_def['values']};\n\n"
# Add interfaces
if interfaces:
ts_content += "// ============================\n"
ts_content += "// Interfaces\n"
ts_content += "// ============================\n\n"
for interface in interfaces:
ts_content += f"export interface {interface['name']} {{\n"
for prop in interface['properties']:
optional_marker = "?" if prop['optional'] else ""
ts_content += f" {prop['name']}{optional_marker}: {prop['type']};\n"
ts_content += "}\n\n"
# Add user union type if we have user types
user_interfaces = [i for i in interfaces if i['name'] in ['Candidate', 'Employer']]
if len(user_interfaces) >= 2:
ts_content += "// ============================\n"
ts_content += "// Union Types\n"
ts_content += "// ============================\n\n"
user_type_names = [i['name'] for i in user_interfaces]
ts_content += f"export type User = {' | '.join(user_type_names)};\n\n"
# Add export statement
ts_content += "// Export all types\n"
ts_content += "export type { };\n"
return ts_content
def compile_typescript(ts_file: str) -> bool:
"""Compile TypeScript file to check for syntax errors"""
print(f"🔧 Compiling TypeScript file to check syntax...")
# Check if TypeScript is available
if not check_typescript_available():
print("⚠️ TypeScript compiler not available - skipping compilation check")
print(" To install: npm install -g typescript")
return True # Don't fail if TS isn't available
# Run TypeScript compiler in check mode
return run_command(
f"npx tsc --noEmit --skipLibCheck {ts_file}",
"TypeScript syntax validation"
)
def main():
"""Main function with command line argument parsing"""
parser = argparse.ArgumentParser(
description='Generate TypeScript types from Pydantic models',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python generate_types.py # Use defaults
python generate_types.py --source models.py --output types.ts # Specify files
python generate_types.py --skip-test # Skip model validation
python generate_types.py --skip-compile # Skip TS compilation
python generate_types.py --source models.py --output types.ts --skip-test --skip-compile
"""
)
parser.add_argument(
'--source', '-s',
default='models.py',
help='Source Python file with Pydantic models (default: models.py)'
)
parser.add_argument(
'--output', '-o',
default='types.ts',
help='Output TypeScript file (default: types.ts)'
)
parser.add_argument(
'--skip-test',
action='store_true',
help='Skip running focused_test.py before generation'
)
parser.add_argument(
'--skip-compile',
action='store_true',
help='Skip TypeScript compilation check after generation'
)
parser.add_argument(
'--version', '-v',
action='version',
version='TypeScript Generator 2.0'
)
args = parser.parse_args()
print("🚀 Enhanced TypeScript Type Generator")
print("=" * 50)
print(f"📁 Source file: {args.source}")
print(f"📁 Output file: {args.output}")
print()
try:
# Step 1: Validate source file exists
if not os.path.exists(args.source):
print(f"❌ Source file '{args.source}' not found")
sys.exit(1)
# Step 2: Run focused test (unless skipped)
if not args.skip_test:
if not run_focused_test():
print("❌ Model validation failed - aborting type generation")
sys.exit(1)
print()
else:
print("⏭️ Skipping model validation test")
print()
# Step 3: Generate TypeScript content
print("🔄 Generating TypeScript types...")
ts_content = generate_typescript_interfaces(args.source)
if ts_content is None:
print("❌ Failed to generate TypeScript content")
sys.exit(1)
# Step 4: Write to output file
with open(args.output, 'w') as f:
f.write(ts_content)
file_size = len(ts_content)
print(f"✅ TypeScript types generated: {args.output} ({file_size} characters)")
# Step 5: Compile TypeScript (unless skipped)
if not args.skip_compile:
print()
if not compile_typescript(args.output):
print("❌ TypeScript compilation failed - check the generated file")
sys.exit(1)
else:
print("⏭️ Skipping TypeScript compilation check")
# Step 6: Success summary
print(f"\n🎉 Type generation completed successfully!")
print("=" * 50)
print(f"✅ Generated {args.output} from {args.source}")
print(f"✅ File size: {file_size} characters")
if not args.skip_test:
print("✅ Model validation passed")
if not args.skip_compile:
print("✅ TypeScript syntax validated")
print(f"\n💡 Usage in your TypeScript project:")
print(f" import {{ Candidate, Employer, Job }} from './{Path(args.output).stem}';")
return True
except KeyboardInterrupt:
print(f"\n⏹️ Type generation cancelled by user")
return False
except Exception as e:
print(f"\n❌ Error generating types: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)