Skip to content

Commit 9c25aab

Browse files
committed
✨ Add discover, detect Python module, package, PYTHONPATH, FastAPI app
1 parent 06c008b commit 9c25aab

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

src/fastapi_cli/discover.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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

Comments
 (0)