almosther isolating soleprint
This commit is contained in:
202
station/tools/modelgen/__main__.py
Normal file
202
station/tools/modelgen/__main__.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Modelgen - Generic Model Generation Tool
|
||||
|
||||
Generates typed models from various sources to various formats.
|
||||
|
||||
Input sources:
|
||||
- Configuration files (soleprint.config.json style)
|
||||
- JSON Schema (planned)
|
||||
- Existing codebases: Django, SQLAlchemy, Prisma (planned - for databrowse)
|
||||
|
||||
Output formats:
|
||||
- pydantic: Pydantic BaseModel classes
|
||||
- django: Django ORM models (planned)
|
||||
- prisma: Prisma schema (planned)
|
||||
- sqlalchemy: SQLAlchemy models (planned)
|
||||
|
||||
Usage:
|
||||
python -m station.tools.modelgen --help
|
||||
python -m station.tools.modelgen from-config -c config.json -o models/ -f pydantic
|
||||
python -m station.tools.modelgen from-schema -s schema.json -o models/ -f pydantic
|
||||
python -m station.tools.modelgen extract -s /path/to/django/app -o models/ -f pydantic
|
||||
|
||||
This is a GENERIC tool. For soleprint-specific builds, use:
|
||||
python build.py dev|deploy
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def cmd_from_config(args):
|
||||
"""Generate models from a configuration file (soleprint.config.json style)."""
|
||||
from .config_loader import load_config
|
||||
from .model_generator import ModelGenerator
|
||||
|
||||
config_path = Path(args.config)
|
||||
if not config_path.exists():
|
||||
print(f"Error: Config file not found: {config_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
output_path = Path(args.output)
|
||||
|
||||
print(f"Loading config: {config_path}")
|
||||
config = load_config(config_path)
|
||||
|
||||
print(f"Generating {args.format} models to: {output_path}")
|
||||
generator = ModelGenerator(
|
||||
config=config,
|
||||
output_path=output_path,
|
||||
output_format=args.format,
|
||||
)
|
||||
result_path = generator.generate()
|
||||
|
||||
print(f"✓ Models generated: {result_path}")
|
||||
|
||||
|
||||
def cmd_from_schema(args):
|
||||
"""Generate models from JSON Schema."""
|
||||
print("Error: from-schema not yet implemented", file=sys.stderr)
|
||||
print("Use from-config with a soleprint.config.json file for now", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_extract(args):
|
||||
"""Extract models from existing codebase (for databrowse graphs)."""
|
||||
print("Error: extract not yet implemented", file=sys.stderr)
|
||||
print(
|
||||
"This will extract models from Django/SQLAlchemy/Prisma codebases.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Use cases:", file=sys.stderr)
|
||||
print(" - Generate browsable graphs for databrowse tool", file=sys.stderr)
|
||||
print(" - Convert between ORM formats", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_list_formats(args):
|
||||
"""List available output formats."""
|
||||
from .model_generator import ModelGenerator
|
||||
|
||||
print("Available output formats:")
|
||||
for fmt in ModelGenerator.available_formats():
|
||||
print(f" - {fmt}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Modelgen - Generic Model Generation Tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# from-config command
|
||||
config_parser = subparsers.add_parser(
|
||||
"from-config",
|
||||
help="Generate models from configuration file",
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--config",
|
||||
"-c",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to configuration file (e.g., soleprint.config.json)",
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Output path (file or directory)",
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--format",
|
||||
"-f",
|
||||
type=str,
|
||||
default="pydantic",
|
||||
choices=["pydantic", "django", "prisma", "sqlalchemy"],
|
||||
help="Output format (default: pydantic)",
|
||||
)
|
||||
config_parser.set_defaults(func=cmd_from_config)
|
||||
|
||||
# from-schema command (placeholder)
|
||||
schema_parser = subparsers.add_parser(
|
||||
"from-schema",
|
||||
help="Generate models from JSON Schema (not yet implemented)",
|
||||
)
|
||||
schema_parser.add_argument(
|
||||
"--schema",
|
||||
"-s",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to JSON Schema file",
|
||||
)
|
||||
schema_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Output path (file or directory)",
|
||||
)
|
||||
schema_parser.add_argument(
|
||||
"--format",
|
||||
"-f",
|
||||
type=str,
|
||||
default="pydantic",
|
||||
choices=["pydantic", "django", "prisma", "sqlalchemy"],
|
||||
help="Output format (default: pydantic)",
|
||||
)
|
||||
schema_parser.set_defaults(func=cmd_from_schema)
|
||||
|
||||
# extract command (placeholder for databrowse)
|
||||
extract_parser = subparsers.add_parser(
|
||||
"extract",
|
||||
help="Extract models from existing codebase (not yet implemented)",
|
||||
)
|
||||
extract_parser.add_argument(
|
||||
"--source",
|
||||
"-s",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to source codebase",
|
||||
)
|
||||
extract_parser.add_argument(
|
||||
"--framework",
|
||||
type=str,
|
||||
choices=["django", "sqlalchemy", "prisma", "auto"],
|
||||
default="auto",
|
||||
help="Source framework to extract from (default: auto-detect)",
|
||||
)
|
||||
extract_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Output path (file or directory)",
|
||||
)
|
||||
extract_parser.add_argument(
|
||||
"--format",
|
||||
"-f",
|
||||
type=str,
|
||||
default="pydantic",
|
||||
choices=["pydantic", "django", "prisma", "sqlalchemy"],
|
||||
help="Output format (default: pydantic)",
|
||||
)
|
||||
extract_parser.set_defaults(func=cmd_extract)
|
||||
|
||||
# list-formats command
|
||||
formats_parser = subparsers.add_parser(
|
||||
"list-formats",
|
||||
help="List available output formats",
|
||||
)
|
||||
formats_parser.set_defaults(func=cmd_list_formats)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,63 +1,82 @@
|
||||
"""
|
||||
Model Generator
|
||||
|
||||
Generates Pydantic models from framework configuration.
|
||||
Generic model generation from configuration files.
|
||||
Supports multiple output formats and is extensible for bidirectional conversion.
|
||||
|
||||
Output formats:
|
||||
- pydantic: Pydantic BaseModel classes
|
||||
- django: Django ORM models (planned)
|
||||
- prisma: Prisma schema (planned)
|
||||
- sqlalchemy: SQLAlchemy models (planned)
|
||||
|
||||
Future: Extract models FROM existing codebases (reverse direction)
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Dict, Type
|
||||
|
||||
from .config_loader import ConfigLoader
|
||||
|
||||
|
||||
class ModelGenerator:
|
||||
"""Generates Pydantic model files from configuration"""
|
||||
class BaseModelWriter(ABC):
|
||||
"""Abstract base for model output writers."""
|
||||
|
||||
def __init__(self, config: ConfigLoader, output_dir: Path):
|
||||
self.config = config
|
||||
self.output_dir = Path(output_dir)
|
||||
@abstractmethod
|
||||
def write(self, config: ConfigLoader, output_path: Path) -> None:
|
||||
"""Write models to the specified path."""
|
||||
pass
|
||||
|
||||
def generate(self):
|
||||
"""Generate all model files"""
|
||||
models_dir = self.output_dir / "models" / "pydantic"
|
||||
models_dir.mkdir(parents=True, exist_ok=True)
|
||||
@abstractmethod
|
||||
def file_extension(self) -> str:
|
||||
"""Return the file extension for this format."""
|
||||
pass
|
||||
|
||||
# Generate __init__.py with all models
|
||||
self._generate_models_file(models_dir / "__init__.py")
|
||||
|
||||
print(f"Generated models in {models_dir}")
|
||||
class PydanticWriter(BaseModelWriter):
|
||||
"""Generates Pydantic model files."""
|
||||
|
||||
def _generate_models_file(self, output_path: Path):
|
||||
"""Generate the main models file"""
|
||||
def file_extension(self) -> str:
|
||||
return ".py"
|
||||
|
||||
def write(self, config: ConfigLoader, output_path: Path) -> None:
|
||||
"""Write Pydantic models to output_path."""
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = self._generate_content(config)
|
||||
output_path.write_text(content)
|
||||
|
||||
def _generate_content(self, config: ConfigLoader) -> str:
|
||||
"""Generate the Pydantic models file content."""
|
||||
|
||||
# Get component names from config
|
||||
config_comp = self.config.get_shared_component('config')
|
||||
data_comp = self.config.get_shared_component('data')
|
||||
config_comp = config.get_shared_component("config")
|
||||
data_comp = config.get_shared_component("data")
|
||||
|
||||
data_flow_sys = self.config.get_system('data_flow')
|
||||
doc_sys = self.config.get_system('documentation')
|
||||
exec_sys = self.config.get_system('execution')
|
||||
data_flow_sys = config.get_system("data_flow")
|
||||
doc_sys = config.get_system("documentation")
|
||||
exec_sys = config.get_system("execution")
|
||||
|
||||
connector_comp = self.config.get_component('data_flow', 'connector')
|
||||
pulse_comp = self.config.get_component('data_flow', 'composed')
|
||||
connector_comp = config.get_component("data_flow", "connector")
|
||||
pulse_comp = config.get_component("data_flow", "composed")
|
||||
|
||||
pattern_comp = self.config.get_component('documentation', 'pattern')
|
||||
maps_comp = self.config.get_component('documentation', 'library')
|
||||
doc_composed = self.config.get_component('documentation', 'composed')
|
||||
pattern_comp = config.get_component("documentation", "pattern")
|
||||
doc_composed = config.get_component("documentation", "composed")
|
||||
|
||||
tool_comp = self.config.get_component('execution', 'utility')
|
||||
monitor_comp = self.config.get_component('execution', 'watcher')
|
||||
cabinet_comp = self.config.get_component('execution', 'container')
|
||||
exec_composed = self.config.get_component('execution', 'composed')
|
||||
tool_comp = config.get_component("execution", "utility")
|
||||
monitor_comp = config.get_component("execution", "watcher")
|
||||
cabinet_comp = config.get_component("execution", "container")
|
||||
exec_composed = config.get_component("execution", "composed")
|
||||
|
||||
# Build the template
|
||||
content = f'''"""
|
||||
Pydantic models - Generated from {self.config.framework.name}.config.json
|
||||
return f'''"""
|
||||
Pydantic models - Generated from {config.framework.name}.config.json
|
||||
|
||||
DO NOT EDIT MANUALLY - Regenerate from config
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional, List, Literal
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@@ -83,8 +102,10 @@ class ToolType(str, Enum):
|
||||
|
||||
# === Shared Components ===
|
||||
|
||||
|
||||
class {config_comp.title}(BaseModel):
|
||||
"""{config_comp.description}. Shared across {data_flow_sys.name}, {exec_sys.name}."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -94,6 +115,7 @@ class {config_comp.title}(BaseModel):
|
||||
|
||||
class {data_comp.title}(BaseModel):
|
||||
"""{data_comp.description}. Shared across all systems."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -104,8 +126,10 @@ class {data_comp.title}(BaseModel):
|
||||
|
||||
# === System-Specific Components ===
|
||||
|
||||
|
||||
class {connector_comp.title}(BaseModel):
|
||||
"""{connector_comp.description} ({data_flow_sys.name})."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -117,6 +141,7 @@ class {connector_comp.title}(BaseModel):
|
||||
|
||||
class {pattern_comp.title}(BaseModel):
|
||||
"""{pattern_comp.description} ({doc_sys.name})."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -127,6 +152,7 @@ class {pattern_comp.title}(BaseModel):
|
||||
|
||||
class {tool_comp.title}(BaseModel):
|
||||
"""{tool_comp.description} ({exec_sys.name})."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -141,6 +167,7 @@ class {tool_comp.title}(BaseModel):
|
||||
|
||||
class {monitor_comp.title}(BaseModel):
|
||||
"""{monitor_comp.description} ({exec_sys.name})."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -150,6 +177,7 @@ class {monitor_comp.title}(BaseModel):
|
||||
|
||||
class {cabinet_comp.title}(BaseModel):
|
||||
"""{cabinet_comp.description} ({exec_sys.name})."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -160,8 +188,10 @@ class {cabinet_comp.title}(BaseModel):
|
||||
|
||||
# === Composed Types ===
|
||||
|
||||
|
||||
class {pulse_comp.title}(BaseModel):
|
||||
"""{pulse_comp.description} ({data_flow_sys.name}). Formula: {pulse_comp.formula}."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -174,6 +204,7 @@ class {pulse_comp.title}(BaseModel):
|
||||
|
||||
class {doc_composed.title}(BaseModel):
|
||||
"""{doc_composed.description} ({doc_sys.name}). Formula: {doc_composed.formula}."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -186,6 +217,7 @@ class {doc_composed.title}(BaseModel):
|
||||
|
||||
class {exec_composed.title}(BaseModel):
|
||||
"""{exec_composed.description} ({exec_sys.name}). Formula: {exec_composed.formula}."""
|
||||
|
||||
name: str # Unique identifier
|
||||
slug: str # URL-friendly identifier
|
||||
title: str # Display title for UI
|
||||
@@ -198,6 +230,7 @@ class {exec_composed.title}(BaseModel):
|
||||
|
||||
# === Collection wrappers for JSON files ===
|
||||
|
||||
|
||||
class {config_comp.title}Collection(BaseModel):
|
||||
items: List[{config_comp.title}] = Field(default_factory=list)
|
||||
|
||||
@@ -238,18 +271,100 @@ class {exec_composed.title}Collection(BaseModel):
|
||||
items: List[{exec_composed.title}] = Field(default_factory=list)
|
||||
'''
|
||||
|
||||
output_path.write_text(content)
|
||||
|
||||
class DjangoWriter(BaseModelWriter):
|
||||
"""Generates Django model files (placeholder)."""
|
||||
|
||||
def file_extension(self) -> str:
|
||||
return ".py"
|
||||
|
||||
def write(self, config: ConfigLoader, output_path: Path) -> None:
|
||||
raise NotImplementedError("Django model generation not yet implemented")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from .config_loader import load_config
|
||||
class PrismaWriter(BaseModelWriter):
|
||||
"""Generates Prisma schema files (placeholder)."""
|
||||
|
||||
# Test with soleprint config
|
||||
config_path = Path(__file__).parent.parent / "soleprint.config.json"
|
||||
config = load_config(config_path)
|
||||
def file_extension(self) -> str:
|
||||
return ".prisma"
|
||||
|
||||
output_dir = Path(__file__).parent.parent / "soleprint-room"
|
||||
generator = ModelGenerator(config, output_dir)
|
||||
generator.generate()
|
||||
def write(self, config: ConfigLoader, output_path: Path) -> None:
|
||||
raise NotImplementedError("Prisma schema generation not yet implemented")
|
||||
|
||||
print("Models generated successfully!")
|
||||
|
||||
class SQLAlchemyWriter(BaseModelWriter):
|
||||
"""Generates SQLAlchemy model files (placeholder)."""
|
||||
|
||||
def file_extension(self) -> str:
|
||||
return ".py"
|
||||
|
||||
def write(self, config: ConfigLoader, output_path: Path) -> None:
|
||||
raise NotImplementedError("SQLAlchemy model generation not yet implemented")
|
||||
|
||||
|
||||
# Registry of available writers
|
||||
WRITERS: Dict[str, Type[BaseModelWriter]] = {
|
||||
"pydantic": PydanticWriter,
|
||||
"django": DjangoWriter,
|
||||
"prisma": PrismaWriter,
|
||||
"sqlalchemy": SQLAlchemyWriter,
|
||||
}
|
||||
|
||||
|
||||
class ModelGenerator:
|
||||
"""
|
||||
Generates typed models from configuration.
|
||||
|
||||
This is the main entry point for model generation.
|
||||
Delegates to format-specific writers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ConfigLoader,
|
||||
output_path: Path,
|
||||
output_format: str = "pydantic",
|
||||
):
|
||||
"""
|
||||
Initialize the generator.
|
||||
|
||||
Args:
|
||||
config: Loaded configuration
|
||||
output_path: Exact path where to write (file or directory depending on format)
|
||||
output_format: Output format (pydantic, django, prisma, sqlalchemy)
|
||||
"""
|
||||
self.config = config
|
||||
self.output_path = Path(output_path)
|
||||
self.output_format = output_format
|
||||
|
||||
if output_format not in WRITERS:
|
||||
raise ValueError(
|
||||
f"Unknown output format: {output_format}. "
|
||||
f"Available: {list(WRITERS.keys())}"
|
||||
)
|
||||
|
||||
self.writer = WRITERS[output_format]()
|
||||
|
||||
def generate(self) -> Path:
|
||||
"""
|
||||
Generate models to the specified output path.
|
||||
|
||||
Returns:
|
||||
Path to the generated file/directory
|
||||
"""
|
||||
# Determine output file path
|
||||
if self.output_path.suffix:
|
||||
# User specified a file path
|
||||
output_file = self.output_path
|
||||
else:
|
||||
# User specified a directory, add default filename
|
||||
output_file = self.output_path / f"__init__{self.writer.file_extension()}"
|
||||
|
||||
self.writer.write(self.config, output_file)
|
||||
print(f"Generated {self.output_format} models: {output_file}")
|
||||
return output_file
|
||||
|
||||
@classmethod
|
||||
def available_formats(cls) -> list:
|
||||
"""Return list of available output formats."""
|
||||
return list(WRITERS.keys())
|
||||
Reference in New Issue
Block a user