|
| 1 | +import importlib |
| 2 | +import sys |
| 3 | +from dataclasses import dataclass |
| 4 | +from logging import getLogger |
| 5 | +from pathlib import Path |
| 6 | +from typing import Union |
| 7 | + |
| 8 | +from fastapi import FastAPI |
| 9 | +from rich import print |
| 10 | +from rich.padding import Padding |
| 11 | +from rich.panel import Panel |
| 12 | +from rich.syntax import Syntax |
| 13 | +from rich.tree import Tree |
| 14 | + |
| 15 | +from fastapi_cli.exceptions import FastAPICLIException |
| 16 | + |
| 17 | +logger = getLogger(__name__) |
| 18 | + |
| 19 | + |
| 20 | +def get_default_path() -> Path: |
| 21 | + path = Path("main.py") |
| 22 | + if path.is_file(): |
| 23 | + return path |
| 24 | + path = Path("app.py") |
| 25 | + if path.is_file(): |
| 26 | + return path |
| 27 | + path = Path("api.py") |
| 28 | + if path.is_file(): |
| 29 | + return path |
| 30 | + path = Path("app/main.py") |
| 31 | + if path.is_file(): |
| 32 | + return path |
| 33 | + path = Path("app/app.py") |
| 34 | + if path.is_file(): |
| 35 | + return path |
| 36 | + path = Path("app/api.py") |
| 37 | + if path.is_file(): |
| 38 | + return path |
| 39 | + raise FastAPICLIException( |
| 40 | + "Could not find a default file to run, please provide an explicit path" |
| 41 | + ) |
| 42 | + |
| 43 | + |
| 44 | +@dataclass |
| 45 | +class ModuleData: |
| 46 | + module_import_str: str |
| 47 | + extra_sys_path: Path |
| 48 | + |
| 49 | + |
| 50 | +def get_module_data_from_path(path: Path) -> ModuleData: |
| 51 | + logger.info( |
| 52 | + "Searching for package file structure from directories with [blue]__init__.py[/blue] files" |
| 53 | + ) |
| 54 | + use_path = path.resolve() |
| 55 | + module_path = use_path |
| 56 | + if use_path.is_file() and use_path.stem == "__init__": |
| 57 | + module_path = use_path.parent |
| 58 | + module_paths = [module_path] |
| 59 | + extra_sys_path = module_path.parent |
| 60 | + for parent in module_path.parents: |
| 61 | + init_path = parent / "__init__.py" |
| 62 | + if init_path.is_file(): |
| 63 | + module_paths.insert(0, parent) |
| 64 | + extra_sys_path = parent.parent |
| 65 | + else: |
| 66 | + break |
| 67 | + logger.info(f"Importing from {extra_sys_path.resolve()}") |
| 68 | + root = module_paths[0] |
| 69 | + name = f"🐍 {root.name}" if root.is_file() else f"📁 {root.name}" |
| 70 | + root_tree = Tree(name) |
| 71 | + if root.is_dir(): |
| 72 | + root_tree.add("[dim]🐍 __init__.py[/dim]") |
| 73 | + tree = root_tree |
| 74 | + for sub_path in module_paths[1:]: |
| 75 | + sub_name = ( |
| 76 | + f"🐍 {sub_path.name}" if sub_path.is_file() else f"📁 {sub_path.name}" |
| 77 | + ) |
| 78 | + tree = tree.add(sub_name) |
| 79 | + if sub_path.is_dir(): |
| 80 | + tree.add("[dim]🐍 __init__.py[/dim]") |
| 81 | + title = "[b green]Python module file[/b green]" |
| 82 | + if len(module_paths) > 1 or module_path.is_dir(): |
| 83 | + title = "[b green]Python package file structure[/b green]" |
| 84 | + panel = Padding( |
| 85 | + Panel( |
| 86 | + root_tree, |
| 87 | + title=title, |
| 88 | + expand=False, |
| 89 | + padding=(1, 2), |
| 90 | + ), |
| 91 | + 1, |
| 92 | + ) |
| 93 | + print(panel) |
| 94 | + module_str = ".".join(p.stem for p in module_paths) |
| 95 | + logger.info(f"Importing module [green]{module_str}[/green]") |
| 96 | + return ModuleData( |
| 97 | + module_import_str=module_str, extra_sys_path=extra_sys_path.resolve() |
| 98 | + ) |
| 99 | + |
| 100 | + |
| 101 | +def get_app_name(*, mod_data: ModuleData, app_name: Union[str, None] = None) -> str: |
| 102 | + try: |
| 103 | + mod = importlib.import_module(mod_data.module_import_str) |
| 104 | + except ImportError as e: |
| 105 | + logger.error(f"Import error: {e}") |
| 106 | + logger.warning( |
| 107 | + "Ensure all the package directories have an [blue]__init__.py[/blue] file" |
| 108 | + ) |
| 109 | + raise |
| 110 | + object_names = dir(mod) |
| 111 | + object_names_set = set(object_names) |
| 112 | + if app_name: |
| 113 | + if app_name not in object_names_set: |
| 114 | + raise FastAPICLIException( |
| 115 | + f"Could not find app name {app_name} in {mod_data.module_import_str}" |
| 116 | + ) |
| 117 | + app = getattr(mod, app_name) |
| 118 | + if not isinstance(app, FastAPI): |
| 119 | + raise FastAPICLIException( |
| 120 | + f"The app name {app_name} in {mod_data.module_import_str} doesn't seem to be a FastAPI app" |
| 121 | + ) |
| 122 | + return app_name |
| 123 | + for preferred_name in ["app", "api"]: |
| 124 | + if preferred_name in object_names_set: |
| 125 | + obj = getattr(mod, preferred_name) |
| 126 | + if isinstance(obj, FastAPI): |
| 127 | + return preferred_name |
| 128 | + for name in object_names: |
| 129 | + obj = getattr(mod, name) |
| 130 | + if isinstance(obj, FastAPI): |
| 131 | + return name |
| 132 | + raise FastAPICLIException( |
| 133 | + "Could not find FastAPI object in module, try using --app" |
| 134 | + ) |
| 135 | + |
| 136 | + |
| 137 | +def get_import_string( |
| 138 | + *, path: Union[Path, None] = None, app_name: Union[str, None] = None |
| 139 | +) -> str: |
| 140 | + if not path: |
| 141 | + path = get_default_path() |
| 142 | + logger.info(f"Using path [blue]{path}[/blue]") |
| 143 | + logger.info(f"Resolved absolute path {path.resolve()}") |
| 144 | + if not path.exists(): |
| 145 | + raise FastAPICLIException(f"Path does not exist {path}") |
| 146 | + mod_data = get_module_data_from_path(path) |
| 147 | + sys.path.insert(0, str(mod_data.extra_sys_path)) |
| 148 | + use_app_name = get_app_name(mod_data=mod_data, app_name=app_name) |
| 149 | + import_example = Syntax( |
| 150 | + f"from {mod_data.module_import_str} import {use_app_name}", "python" |
| 151 | + ) |
| 152 | + import_panel = Padding( |
| 153 | + Panel( |
| 154 | + import_example, |
| 155 | + title="[b green]Importable FastAPI object[/b green]", |
| 156 | + expand=False, |
| 157 | + padding=(1, 2), |
| 158 | + ), |
| 159 | + 1, |
| 160 | + ) |
| 161 | + logger.info("Found importable FastAPI object") |
| 162 | + print(import_panel) |
| 163 | + import_string = f"{mod_data.module_import_str}:{use_app_name}" |
| 164 | + logger.info(f"Using import string [b green]{import_string}[/b green]") |
| 165 | + return import_string |
0 commit comments