Coverage for src/fastapi_new/new.py: 100%

104 statements  

« prev     ^ index     » next       coverage.py v7.11.2, created at 2026-11-04 19:57 +0000

1import pathlib 2hbibjbkblbmbnbob

2import shutil 2hbibjbkblbmbnbob

3import subprocess 2hbibjbkblbmbnbob

4from dataclasses import dataclass 2hbibjbkblbmbnbob

5from typing import Annotated 2hbibjbkblbmbnbob

6 

7import typer 2hbibjbkblbmbnbob

8from rich_toolkit import RichToolkit 2hbibjbkblbmbnbob

9 

10from .utils.cli import get_rich_toolkit 2hbibjbkblbmbnbob

11 

12TEMPLATE_CONTENT = """from fastapi import FastAPI 2hbibjbkblbmbnbob

13app = FastAPI() 

14 

15@app.get("/") 

16def main(): 

17 return {"message": "Hello World"} 

18""" 

19 

20 

21@dataclass 2hbibjbkblbmbnbob

22class ProjectConfig: 2hbibjbkblbmbnbob

23 name: str 2hbibjbkblbmbnbob

24 path: pathlib.Path 2hbibjbkblbmbnbob

25 python: str | None = None 2hbibjbkblbmbnbob

26 

27 

28def _generate_readme(project_name: str) -> str: 2hbibjbkblbmbnbob

29 return f"""# {project_name} 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

30 

31A project created with FastAPI CLI. 

32 

33## Quick Start 

34 

35### Start the development server 

36 

37```bash 

38uv run fastapi dev 

39``` 

40 

41Visit http://localhost:8000 

42 

43### Deploy to FastAPI Cloud 

44 

45Sign up and log in at https://fastapicloud.com, then deploy with: 

46 

47```bash 

48uv run fastapi deploy 

49``` 

50 

51## Project Structure 

52 

53- `main.py` - Your FastAPI application 

54- `pyproject.toml` - Project dependencies 

55 

56## Learn More 

57 

58- [FastAPI Documentation](https://fastapi.tiangolo.com) 

59- [FastAPI Cloud](https://fastapicloud.com) 

60""" 

61 

62 

63def _exit_with_error(toolkit: RichToolkit, error_msg: str) -> None: 2hbibjbkblbmbnbob

64 toolkit.print(f"[bold red]Error:[/bold red] {error_msg}", tag="error") 2- [ 4 $ xb~ pb. ] 5 % ybabqb/ ^ 6 ' zbbbrb: _ 7 ( Abcbsb; ` 8 ) Bbdbtb= { 9 * Cbebub? | ! + Dbfbvb@ } # , Ebgbwb

65 raise typer.Exit(code=1) 2- [ 4 $ xb~ pb. ] 5 % ybabqb/ ^ 6 ' zbbbrb: _ 7 ( Abcbsb; ` 8 ) Bbdbtb= { 9 * Cbebub? | ! + Dbfbvb@ } # , Ebgbwb

66 

67 

68def _validate_python_version(python: str | None) -> str | None: 2hbibjbkblbmbnbob

69 """ 

70 Validate Python version is >= 3.10. 

71 Returns error message if < 3.10, None otherwise. 

72 Let uv handle malformed versions or versions it can't find. 

73 """ 

74 if not python: 2J a K - [ 4 q r $ i ~ L M b N . ] 5 s t % j abO u c P / ^ 6 v w ' k bbQ R d S : _ 7 x y ( l cbT z e U ; ` 8 A B ) m dbV W f X = { 9 C D * n ebY Z g 0 ? | ! E F + o fb1 G h 2 @ } # H I , p gb3

75 return None 1JK-[4qrLMN.]5stOuP/^6vwQRS:_7xyTzU;`8ABVWX={9CDYZ0?|!EF1G2@}#HI3

76 

77 try: 2a $ i ~ b % j abc ' k bbd ( l cbe ) m dbf * n ebg + o fbh , p gb

78 parts = python.split(".") 2a $ i ~ b % j abc ' k bbd ( l cbe ) m dbf * n ebg + o fbh , p gb

79 if len(parts) < 2: 2a $ i ~ b % j abc ' k bbd ( l cbe ) m dbf * n ebg + o fbh , p gb

80 return None # Let uv handle malformed version 1ijklmnop

81 major, minor = int(parts[0]), int(parts[1]) 2a $ ~ b % abc ' bbd ( cbe ) dbf * ebg + fbh , gb

82 

83 if major < 3 or (major == 3 and minor < 10): 2a ~ b abc bbd cbe dbf ebg fbh gb

84 return f"Python {python} is not supported. FastAPI requires Python 3.10 or higher." 2~ abbbcbdbebfbgb

85 except (ValueError, IndexError): 1$%'()*+,

86 # Malformed version - let uv handle the error 

87 pass 1$%'()*+,

88 

89 return None 1a$b%c'd(e)f*g+h,

90 

91 

92def _setup(toolkit: RichToolkit, config: ProjectConfig) -> None: 2hbibjbkblbmbnbob

93 error = _validate_python_version(config.python) 2J a K - [ 4 q r $ i ~ L M b N . ] 5 s t % j abO u c P / ^ 6 v w ' k bbQ R d S : _ 7 x y ( l cbT z e U ; ` 8 A B ) m dbV W f X = { 9 C D * n ebY Z g 0 ? | ! E F + o fb1 G h 2 @ } # H I , p gb3

94 if error: 2J a K - [ 4 q r $ i ~ L M b N . ] 5 s t % j abO u c P / ^ 6 v w ' k bbQ R d S : _ 7 x y ( l cbT z e U ; ` 8 A B ) m dbV W f X = { 9 C D * n ebY Z g 0 ? | ! E F + o fb1 G h 2 @ } # H I , p gb3

95 _exit_with_error(toolkit, error) 2~ abbbcbdbebfbgb

96 

97 msg = "Setting up environment with uv" 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

98 

99 if config.python: 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

100 msg += f" (Python {config.python})" 1a$ib%jc'kd(le)mf*ng+oh,p

101 

102 toolkit.print(msg, tag="env") 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

103 

104 # If config.name is provided, create in subdirectory; otherwise init in current dir 

105 # uv will infer the project name from the directory name 

106 if config.path == pathlib.Path.cwd(): 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

107 init_cmd = ["uv", "init", "--bare"] 1qrstvwxyABCDEFHI

108 else: 

109 init_cmd = ["uv", "init", "--bare", str(config.path)] 1JaK-[4$iLMbN.]5%jOucP/^6'kQRdS:_7(lTzeU;`8)mVWfX={9*nYZg0?|!+o1Gh2@}#,p3

110 

111 if config.python: 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

112 init_cmd.extend(["--python", config.python]) 1a$ib%jc'kd(le)mf*ng+oh,p

113 

114 try: 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

115 subprocess.run(init_cmd, check=True, capture_output=True) 1JaK-[4qr$iLMbN.]5st%jOucP/^6vw'kQRdS:_7xy(lTzeU;`8AB)mVWfX={9CD*nYZg0?|!EF+o1Gh2@}#HI,p3

116 except subprocess.CalledProcessError as e: 1[$]%^'_(`){*|+},

117 stderr = e.stderr.decode() if e.stderr else "No details available" 1[$]%^'_(`){*|+},

118 _exit_with_error(toolkit, f"Failed to initialize project with uv. {stderr}") 1[$]%^'_(`){*|+},

119 

120 

121def _install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None: 2hbibjbkblbmbnbob

122 toolkit.print("Installing dependencies...", tag="deps") 1JaK-4qriLMbN.5stjOucP/6vwkQRdS:7xylTzeU;8ABmVWfX=9CDnYZg0?!EFo1Gh2@#HIp3

123 

124 try: 1JaK-4qriLMbN.5stjOucP/6vwkQRdS:7xylTzeU;8ABmVWfX=9CDnYZg0?!EFo1Gh2@#HIp3

125 subprocess.run( 1JaK-4qriLMbN.5stjOucP/6vwkQRdS:7xylTzeU;8ABmVWfX=9CDnYZg0?!EFo1Gh2@#HIp3

126 ["uv", "add", "fastapi[standard]"], 

127 check=True, 

128 capture_output=True, 

129 cwd=config.path, 

130 ) 

131 except subprocess.CalledProcessError as e: 1-./:;=?@

132 stderr = e.stderr.decode() if e.stderr else "No details available" 1-./:;=?@

133 _exit_with_error(toolkit, f"Failed to install dependencies. {stderr}") 1-./:;=?@

134 

135 

136def _write_template_files(toolkit: RichToolkit, config: ProjectConfig) -> None: 2hbibjbkblbmbnbob

137 toolkit.print("Writing template files...", tag="template") 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

138 readme_content = _generate_readme(config.name) 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

139 

140 try: 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

141 (config.path / "main.py").write_text(TEMPLATE_CONTENT) 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

142 (config.path / "README.md").write_text(readme_content) 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

143 except Exception as e: 1456789!#

144 _exit_with_error(toolkit, f"Failed to write template files. {str(e)}") 1456789!#

145 

146 

147def new( 2hbibjbu kblbz mbnbobG

148 ctx: typer.Context, 

149 project: Annotated[ 

150 str | None, 

151 typer.Argument( 

152 help="The name/path of the new FastAPI project. If not provided, initializes in the current directory.", 

153 ), 

154 ] = None, 

155 python: Annotated[ 

156 str | None, 

157 typer.Option( 

158 "--python", 

159 "-p", 

160 help="Specify the Python version for the new project (e.g., 3.14). Must be 3.10 or higher.", 

161 ), 

162 ] = None, 

163) -> None: 

164 if project: 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

165 path = (pathlib.Path.cwd() / project).resolve() 2J a K - [ 4 r $ i xb~ pbL M b N . ] 5 t % j ybabqbO u c P / ^ 6 w ' k zbbbrbQ R d S : _ 7 y ( l AbcbsbT z e U ; ` 8 B ) m BbdbtbV W f X = { 9 D * n CbebubY Z g 0 ? | ! F + o Dbfbvb1 G h 2 @ } # I , p Ebgbwb3

166 else: 

167 path = pathlib.Path.cwd() 1qsvxACEH

168 

169 current_dir = path == pathlib.Path.cwd() 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

170 

171 config = ProjectConfig( 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

172 name=path.name, 

173 path=path, 

174 python=python, 

175 ) 

176 

177 with get_rich_toolkit() as toolkit: 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

178 toolkit.print_title("Creating a new project 🚀", tag="FastAPI") 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

179 

180 toolkit.print_line() 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

181 

182 if current_dir: 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

183 toolkit.print( 1qrstvwxyABCDEFHI

184 f"[yellow]⚠️ Initializing in current directory: {config.path}[/yellow]", 

185 tag="warning", 

186 ) 

187 toolkit.print_line() 1qrstvwxyABCDEFHI

188 

189 # Check if project directory already exists (only for new subdirectory) 

190 if not current_dir and config.path.exists(): 2J a K - [ 4 q r $ i xb~ pbL M b N . ] 5 s t % j ybabqbO u c P / ^ 6 v w ' k zbbbrbQ R d S : _ 7 x y ( l AbcbsbT z e U ; ` 8 A B ) m BbdbtbV W f X = { 9 C D * n CbebubY Z g 0 ? | ! E F + o Dbfbvb1 G h 2 @ } # H I , p Ebgbwb3

191 _exit_with_error(toolkit, f"Directory '{config.name}' already exists.") 2xbybzbAbBbCbDbEb

192 

193 if shutil.which("uv") is None: 2J a K - [ 4 q r $ i ~ pbL M b N . ] 5 s t % j abqbO u c P / ^ 6 v w ' k bbrbQ R d S : _ 7 x y ( l cbsbT z e U ; ` 8 A B ) m dbtbV W f X = { 9 C D * n ebubY Z g 0 ? | ! E F + o fbvb1 G h 2 @ } # H I , p gbwb3

194 _exit_with_error( 2pbqbrbsbtbubvbwb

195 toolkit, 

196 "uv is required to create new projects. Install it from https://docs.astral.sh/uv/getting-started/installation/", 

197 ) 

198 

199 _setup(toolkit, config) 2J a K - [ 4 q r $ i ~ L M b N . ] 5 s t % j abO u c P / ^ 6 v w ' k bbQ R d S : _ 7 x y ( l cbT z e U ; ` 8 A B ) m dbV W f X = { 9 C D * n ebY Z g 0 ? | ! E F + o fb1 G h 2 @ } # H I , p gb3

200 

201 toolkit.print_line() 1JaK-4qriLMbN.5stjOucP/6vwkQRdS:7xylTzeU;8ABmVWfX=9CDnYZg0?!EFo1Gh2@#HIp3

202 

203 _install_dependencies(toolkit, config) 1JaK-4qriLMbN.5stjOucP/6vwkQRdS:7xylTzeU;8ABmVWfX=9CDnYZg0?!EFo1Gh2@#HIp3

204 

205 toolkit.print_line() 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

206 

207 _write_template_files(toolkit, config) 1JaK4qriLMbN5stjOucP6vwkQRdS7xylTzeU8ABmVWfX9CDnYZg0!EFo1Gh2#HIp3

208 

209 toolkit.print_line() 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

210 

211 # Print success message 

212 if not current_dir: 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

213 toolkit.print( 1JaKiLMbNjOucPkQRdSlTzeUmVWfXnYZg0o1Gh2p3

214 f"[bold green]✨ Success![/bold green] Created FastAPI project: [cyan]{config.name}[/cyan]", 

215 tag="success", 

216 ) 

217 

218 toolkit.print_line() 1JaKiLMbNjOucPkQRdSlTzeUmVWfXnYZg0o1Gh2p3

219 

220 toolkit.print("[bold]Next steps:[/bold]") 1JaKiLMbNjOucPkQRdSlTzeUmVWfXnYZg0o1Gh2p3

221 toolkit.print(f" [dim]$[/dim] cd {config.name}") 1JaKiLMbNjOucPkQRdSlTzeUmVWfXnYZg0o1Gh2p3

222 toolkit.print(" [dim]$[/dim] uv run fastapi dev") 1JaKiLMbNjOucPkQRdSlTzeUmVWfXnYZg0o1Gh2p3

223 else: 

224 toolkit.print( 1qrstvwxyABCDEFHI

225 "[bold green]✨ Success![/bold green] Initialized FastAPI project in current directory", 

226 tag="success", 

227 ) 

228 

229 toolkit.print_line() 1qrstvwxyABCDEFHI

230 

231 toolkit.print("[bold]Next steps:[/bold]") 1qrstvwxyABCDEFHI

232 toolkit.print(" [dim]$[/dim] uv run fastapi dev") 1qrstvwxyABCDEFHI

233 

234 toolkit.print_line() 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

235 

236 toolkit.print("Visit [blue]http://localhost:8000[/blue]") 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

237 

238 toolkit.print_line() 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

239 

240 toolkit.print("[bold]Deploy to FastAPI Cloud:[/bold]") 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

241 toolkit.print(" [dim]$[/dim] uv run fastapi deploy") 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

242 

243 toolkit.print_line() 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

244 

245 toolkit.print( 1JaKqriLMbNstjOucPvwkQRdSxylTzeUABmVWfXCDnYZg0EFo1Gh2HIp3

246 "[dim]💡 Tip: Use 'uv run' to automatically use the project's environment[/dim]" 

247 )