diff --git a/src/ohmyapi/cli.py b/src/ohmyapi/cli.py index 37727e0..42572a1 100644 --- a/src/ohmyapi/cli.py +++ b/src/ohmyapi/cli.py @@ -9,6 +9,9 @@ import typer import uvicorn from ohmyapi.core import runtime, scaffolding +from ohmyapi.core.logging import setup_logging + +logger = setup_logging() app = typer.Typer( help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM." @@ -19,18 +22,24 @@ app = typer.Typer( def startproject(name: str): """Create a new OhMyAPI project in the given directory.""" scaffolding.startproject(name) + logger.info(f"✅ Project '{name}' created successfully.") + logger.info(f"🔧 Next, configure your project in {name}/settings.py") @app.command() def startapp(app_name: str, root: str = "."): """Create a new app with the given name in your OhMyAPI project.""" scaffolding.startapp(app_name, root) + print(f"✅ App '{app_name}' created in project '{root}' successfully.") + print(f"🔧 Remember to add '{app_name}' to your INSTALLED_APPS!") @app.command() def dockerize(root: str = "."): """Create template Dockerfile and docker-compose.yml.""" scaffolding.copy_static("docker", root) + logger.info(f"✅ Templates created successfully.") + logger.info(f"🔧 Next, run `docker compose up -d --build`") @app.command() @@ -42,7 +51,7 @@ def serve(root: str = ".", host="127.0.0.1", port=8000): project = runtime.Project(project_path) app_instance = project.app() app_instance = project.configure_app(app_instance) - uvicorn.run(app_instance, host=host, port=int(port), reload=False) + uvicorn.run(app_instance, host=host, port=int(port), reload=False, log_config=None) @app.command() @@ -107,6 +116,8 @@ def makemigrations(app: str = "*", name: str = "auto", root: str = "."): asyncio.run(project.makemigrations(app_label=app, name=name)) else: asyncio.run(project.makemigrations(app_label=app, name=name)) + logger.info(f"✅ Migrations created successfully.") + logger.info(f"🔧 To migrate the DB, run `ohmyapi migrate`, next.") @app.command() @@ -121,6 +132,7 @@ def migrate(app: str = "*", root: str = "."): asyncio.run(project.migrate(app)) else: asyncio.run(project.migrate(app)) + logger.info(f"✅ Migrations ran successfully.") @app.command() diff --git a/src/ohmyapi/core/logging.py b/src/ohmyapi/core/logging.py new file mode 100644 index 0000000..ca05220 --- /dev/null +++ b/src/ohmyapi/core/logging.py @@ -0,0 +1,38 @@ +import logging +import os +import sys + + +def setup_logging(): + """Configure unified logging for ohmyapi + FastAPI/Uvicorn.""" + log_level = os.getenv("OHMYAPI_LOG_LEVEL", "INFO").upper() + level = getattr(logging, log_level, logging.INFO) + + # Root logger (affects FastAPI, uvicorn, etc.) + logging.basicConfig( + level=level, + format="[%(asctime)s] [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[ + logging.StreamHandler(sys.stdout) + ] + ) + + # Separate ohmyapi logger (optional) + logger = logging.getLogger("ohmyapi") + + # Direct warnings/errors to stderr + class LevelFilter(logging.Filter): + def filter(self, record): + # Send warnings+ to stderr, everything else to stdout + if record.levelno >= logging.WARNING: + record.stream = sys.stderr + else: + record.stream = sys.stdout + return True + + for handler in logger.handlers: + handler.addFilter(LevelFilter()) + + logger.setLevel(level) + return logger diff --git a/src/ohmyapi/core/runtime.py b/src/ohmyapi/core/runtime.py index 010c67d..9a920f8 100644 --- a/src/ohmyapi/core/runtime.py +++ b/src/ohmyapi/core/runtime.py @@ -15,8 +15,11 @@ from aerich.exceptions import NotInitedError from fastapi import APIRouter, FastAPI from tortoise import Tortoise +from ohmyapi.core.logging import setup_logging from ohmyapi.db.model import Model +logger = setup_logging() + class Project: """ @@ -29,6 +32,7 @@ class Project: """ def __init__(self, project_path: str): + logger.debug(f"Loading project: {project_path}") self.project_path = Path(project_path).resolve() self._apps: Dict[str, App] = {} self.migrations_dir = self.project_path / "migrations" @@ -238,6 +242,7 @@ class App: # Import the app, so its __init__.py runs. mod: ModuleType = importlib.import_module(name) + logger.debug(f"Loading app: {self.name}") self.__load_models(f"{self.name}.models") self.__load_routes(f"{self.name}.routes") self.__load_middlewares(f"{self.name}.middlewares") @@ -258,7 +263,6 @@ class App: try: importlib.import_module(mod_name) except ModuleNotFoundError: - print(f"no models detected: {mod_name}") return # Acoid duplicates. @@ -276,9 +280,11 @@ class App: and issubclass(value, Model) and not name == Model.__name__ ): + # monkey-patch __module__ to point to well-known aliases value.__module__ = value.__module__.replace("ohmyapi.builtin.", "ohmyapi_") if value.__module__.startswith(mod_name): self._models[mod_name] = self._models.get(mod_name, []) + [value] + logger.debug(f" - Model: {mod_name} -> {name}") # if it's a package, recurse into submodules if hasattr(mod, "__path__"): @@ -300,7 +306,6 @@ class App: try: importlib.import_module(mod_name) except ModuleNotFound: - print(f"no routes detected: {mod_name}") return # Avoid duplicates. @@ -315,6 +320,7 @@ class App: for name, value in vars(mod).copy().items(): if isinstance(value, APIRouter) and not name == APIRouter.__name__: self._routers[mod_name] = self._routers.get(mod_name, []) + [value] + logger.debug(f" - Router: {mod_name} -> {name} -> {value.routes}") # if it's a package, recurse into submodules if hasattr(mod, "__path__"): @@ -330,7 +336,6 @@ class App: try: mod = importlib.import_module(mod_name) except ModuleNotFoundError: - print(f"no middlewares detected: {mod_name}") return getter = getattr(mod, "get", None) diff --git a/src/ohmyapi/core/scaffolding.py b/src/ohmyapi/core/scaffolding.py index c637b5a..2dddeaf 100644 --- a/src/ohmyapi/core/scaffolding.py +++ b/src/ohmyapi/core/scaffolding.py @@ -2,12 +2,16 @@ from pathlib import Path from jinja2 import Environment, FileSystemLoader +from ohmyapi.core.logging import setup_logging + import shutil # Base templates directory TEMPLATE_DIR = Path(__file__).parent / "templates" env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR))) +logger = setup_logging() + def render_template_file(template_path: Path, context: dict, output_path: Path): """Render a single Jinja2 template file to disk.""" @@ -60,7 +64,7 @@ def copy_static(dir_name: str, target_dir: Path): template_dir = TEMPLATE_DIR / dir_name target_dir = Path(target_dir) if not template_dir.exists(): - print(f"no templates found under: {dir_name}") + logger.error(f"no templates found under: {dir_name}") return for root, _, files in template_dir.walk(): @@ -69,19 +73,15 @@ def copy_static(dir_name: str, target_dir: Path): src = root_path / file dst = target_dir / file if dst.exists(): - print(f"⛔ File exists, skipping: {dst}") + logger.warning(f"⛔ File exists, skipping: {dst}") continue - shutil.copy(src, ".") - print(f"✅ Templates created successfully.") - print(f"🔧 Next, run `docker compose up -d --build`") + shutil.copy(src, dst) def startproject(name: str): """Create a new project: flat structure, all project templates go into /""" target_dir = Path(name).resolve() target_dir.mkdir(exist_ok=True) render_template_dir("project", target_dir, {"project_name": name}) - print(f"✅ Project '{name}' created successfully.") - print(f"🔧 Next, configure your project in {target_dir / 'settings.py'}") def startapp(name: str, project: str): @@ -94,5 +94,3 @@ def startapp(name: str, project: str): {"project_name": target_dir.resolve().name, "app_name": name}, subdir_name=name, ) - print(f"✅ App '{name}' created in project '{target_dir}' successfully.") - print(f"🔧 Remember to add '{name}' to your INSTALLED_APPS!")