You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

133 lines
3.9 KiB

  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 List, Union
  7. from fastapi_cli.exceptions import FastAPICLIException
  8. logger = getLogger(__name__)
  9. try:
  10. from fastapi import FastAPI
  11. except ImportError: # pragma: no cover
  12. FastAPI = None # type: ignore[misc, assignment]
  13. def get_default_path() -> Path:
  14. potential_paths = (
  15. "main.py",
  16. "app.py",
  17. "api.py",
  18. "app/main.py",
  19. "app/app.py",
  20. "app/api.py",
  21. )
  22. for full_path in potential_paths:
  23. path = Path(full_path)
  24. if path.is_file():
  25. return path
  26. raise FastAPICLIException(
  27. "Could not find a default file to run, please provide an explicit path"
  28. )
  29. @dataclass
  30. class ModuleData:
  31. module_import_str: str
  32. extra_sys_path: Path
  33. module_paths: List[Path]
  34. def get_module_data_from_path(path: Path) -> ModuleData:
  35. use_path = path.resolve()
  36. module_path = use_path
  37. if use_path.is_file() and use_path.stem == "__init__":
  38. module_path = use_path.parent
  39. module_paths = [module_path]
  40. extra_sys_path = module_path.parent
  41. for parent in module_path.parents:
  42. init_path = parent / "__init__.py"
  43. if init_path.is_file():
  44. module_paths.insert(0, parent)
  45. extra_sys_path = parent.parent
  46. else:
  47. break
  48. module_str = ".".join(p.stem for p in module_paths)
  49. return ModuleData(
  50. module_import_str=module_str,
  51. extra_sys_path=extra_sys_path.resolve(),
  52. module_paths=module_paths,
  53. )
  54. def get_app_name(*, mod_data: ModuleData, app_name: Union[str, None] = None) -> str:
  55. try:
  56. mod = importlib.import_module(mod_data.module_import_str)
  57. except (ImportError, ValueError) as e:
  58. logger.error(f"Import error: {e}")
  59. logger.warning(
  60. "Ensure all the package directories have an [blue]__init__.py[/blue] file"
  61. )
  62. raise
  63. if not FastAPI: # type: ignore[truthy-function]
  64. raise FastAPICLIException(
  65. "Could not import FastAPI, try running 'pip install fastapi'"
  66. ) from None
  67. object_names = dir(mod)
  68. object_names_set = set(object_names)
  69. if app_name:
  70. if app_name not in object_names_set:
  71. raise FastAPICLIException(
  72. f"Could not find app name {app_name} in {mod_data.module_import_str}"
  73. )
  74. app = getattr(mod, app_name)
  75. if not isinstance(app, FastAPI):
  76. raise FastAPICLIException(
  77. f"The app name {app_name} in {mod_data.module_import_str} doesn't seem to be a FastAPI app"
  78. )
  79. return app_name
  80. for preferred_name in ["app", "api"]:
  81. if preferred_name in object_names_set:
  82. obj = getattr(mod, preferred_name)
  83. if isinstance(obj, FastAPI):
  84. return preferred_name
  85. for name in object_names:
  86. obj = getattr(mod, name)
  87. if isinstance(obj, FastAPI):
  88. return name
  89. raise FastAPICLIException("Could not find FastAPI app in module, try using --app")
  90. @dataclass
  91. class ImportData:
  92. app_name: str
  93. module_data: ModuleData
  94. import_string: str
  95. def get_import_data(
  96. *, path: Union[Path, None] = None, app_name: Union[str, None] = None
  97. ) -> ImportData:
  98. if not path:
  99. path = get_default_path()
  100. logger.debug(f"Using path [blue]{path}[/blue]")
  101. logger.debug(f"Resolved absolute path {path.resolve()}")
  102. if not path.exists():
  103. raise FastAPICLIException(f"Path does not exist {path}")
  104. mod_data = get_module_data_from_path(path)
  105. sys.path.insert(0, str(mod_data.extra_sys_path))
  106. use_app_name = get_app_name(mod_data=mod_data, app_name=app_name)
  107. import_string = f"{mod_data.module_import_str}:{use_app_name}"
  108. return ImportData(
  109. app_name=use_app_name, module_data=mod_data, import_string=import_string
  110. )