Skip to content

Commit 17f1256

Browse files
committed
✨ Add waitlist form
1 parent a1731b5 commit 17f1256

4 files changed

Lines changed: 209 additions & 13 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ dependencies = [
3535
"uvicorn[standard] >= 0.15.0",
3636
"rignore >= 0.5.1",
3737
"httpx >= 0.27.0,< 0.28.0",
38-
"rich-toolkit >= 0.14.3",
39-
"pydantic >= 1.6.1",
38+
"rich-toolkit >= 0.14.5",
39+
"pydantic[email] >= 1.6.1",
4040
]
4141

4242
[project.optional-dependencies]

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import contextlib
12
import json
23
import logging
4+
import subprocess
35
import tarfile
46
import tempfile
57
import time
@@ -12,7 +14,7 @@
1214
import rignore
1315
import typer
1416
from httpx import Client
15-
from pydantic import BaseModel
17+
from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
1618
from rich.text import Text
1719
from rich_toolkit import RichToolkit
1820
from rich_toolkit.menu import Option
@@ -385,6 +387,118 @@ def _setup_environment_variables(toolkit: RichToolkit, app_id: str) -> None:
385387
progress.log("Environment variables set up successfully!")
386388

387389

390+
class SignupToWaitingList(BaseModel):
391+
email: EmailStr
392+
name: str | None = None
393+
organization: str | None = None
394+
role: str | None = None
395+
team_size: str | None = None
396+
location: str | None = None
397+
use_case: str | None = None
398+
secret_code: str | None = None
399+
400+
401+
def _send_waitlist_form(
402+
result: SignupToWaitingList,
403+
toolkit: RichToolkit,
404+
) -> None:
405+
with toolkit.progress("Sending your request...") as progress:
406+
with APIClient() as client:
407+
with handle_http_errors(progress):
408+
response = client.post(
409+
"/users/waiting-list", json=result.model_dump(mode="json")
410+
)
411+
412+
response.raise_for_status()
413+
414+
progress.log("Let's go! Thanks for your interest in FastAPI Cloud! 🚀")
415+
416+
417+
def _waitlist_form(toolkit: RichToolkit) -> None:
418+
from rich_toolkit.form import Form
419+
420+
toolkit.print(
421+
"We're currently in private beta. If you want to be notified when we launch, please fill out the form below.",
422+
tag="waitlist",
423+
)
424+
425+
toolkit.print_line()
426+
427+
email = toolkit.input(
428+
"Enter your email:",
429+
required=True,
430+
validator=TypeAdapter(EmailStr),
431+
)
432+
433+
toolkit.print_line()
434+
435+
result = SignupToWaitingList(email=email)
436+
437+
if toolkit.confirm(
438+
"Do you want to get access faster by giving us more information?",
439+
tag="waitlist",
440+
):
441+
toolkit.print_line()
442+
form = Form("Waitlist form", style=toolkit.style)
443+
444+
form.add_input("name", label="Name", placeholder="John Doe")
445+
form.add_input("organization", label="Organization", placeholder="Acme Inc.")
446+
form.add_input("team", label="Team", placeholder="Team A")
447+
form.add_input("role", label="Role", placeholder="Developer")
448+
form.add_input("location", label="Location", placeholder="San Francisco")
449+
form.add_input(
450+
"use_case",
451+
label="How do you plan to use FastAPI Cloud?",
452+
placeholder="I'm building a web app",
453+
)
454+
form.add_input("secret_code", label="Secret code", placeholder="123456")
455+
456+
result = form.run() # type: ignore
457+
458+
try:
459+
result = SignupToWaitingList.model_validate(
460+
{
461+
"email": email,
462+
**result, # type: ignore
463+
}
464+
)
465+
except ValidationError as e:
466+
toolkit.print(
467+
"[error]Invalid form data. Please try again.[/]",
468+
)
469+
470+
return
471+
472+
toolkit.print_line()
473+
474+
if toolkit.confirm(
475+
(
476+
"Do you agree to\n"
477+
"- Terms of Service: [link=https://fastapicloud.com/legal/terms]https://fastapicloud.com/legal/terms[/link]\n"
478+
"- Privacy Policy: [link=https://fastapicloud.com/legal/privacy-policy]https://fastapicloud.com/legal/privacy-policy[/link]\n"
479+
),
480+
tag="terms",
481+
):
482+
toolkit.print_line()
483+
484+
_send_waitlist_form(
485+
result,
486+
toolkit,
487+
)
488+
489+
with contextlib.suppress(Exception):
490+
subprocess.run(
491+
["open", "raycast://confetti"],
492+
stdout=subprocess.DEVNULL,
493+
stderr=subprocess.DEVNULL,
494+
check=False,
495+
)
496+
497+
toolkit.print_line()
498+
499+
toolkit.print("Thank you for your interest in FastAPI Cloud! 🚀")
500+
501+
388502
def deploy(
389503
path: Annotated[
390504
Union[Path, None],
@@ -401,17 +515,14 @@ def deploy(
401515
"""
402516

403517
with get_rich_toolkit() as toolkit:
404-
toolkit.print_title("Starting deployment", tag="FastAPI")
405-
toolkit.print_line()
406-
407518
if not is_logged_in():
408-
toolkit.print(
409-
"No credentials found. Use [blue]`fastapi login`[/] to login.",
410-
tag="auth",
411-
)
519+
_waitlist_form(toolkit)
412520

413521
raise typer.Exit(1)
414522

523+
toolkit.print_title("Starting deployment", tag="FastAPI")
524+
toolkit.print_line()
525+
415526
path_to_deploy = path or Path.cwd()
416527

417528
app_config = get_app_config(path_to_deploy)

tests/test_cli_deploy.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import random
23
import string
34
from pathlib import Path
@@ -58,11 +59,94 @@ def _get_random_deployment(
5859
}
5960

6061

61-
def test_shows_error_when_not_logged_in(logged_out_cli: None) -> None:
62-
result = runner.invoke(app, ["deploy"])
62+
@pytest.mark.respx(base_url=settings.base_api_url)
63+
def test_shows_waitlist_form_when_not_logged_in(
64+
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
65+
) -> None:
66+
steps = [*"some@example.com", Keys.ENTER, Keys.RIGHT_ARROW, Keys.ENTER, Keys.ENTER]
67+
68+
respx_mock.post(
69+
"/users/waiting-list",
70+
json={
71+
"email": "some@example.com",
72+
"location": None,
73+
"name": None,
74+
"organization": None,
75+
"role": None,
76+
"secret_code": None,
77+
"team_size": None,
78+
"use_case": None,
79+
},
80+
).mock(return_value=Response(200))
81+
82+
with changing_dir(tmp_path), patch(
83+
"rich_toolkit.menu.click.getchar"
84+
) as mock_getchar:
85+
mock_getchar.side_effect = steps
86+
87+
result = runner.invoke(app, ["deploy"])
88+
89+
assert result.exit_code == 1
90+
assert "We're currently in private beta" in result.output
91+
assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output
92+
93+
94+
@pytest.mark.respx(base_url=settings.base_api_url)
95+
def test_shows_waitlist_form_when_not_logged_in_longer_flow(
96+
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
97+
) -> None:
98+
steps = [
99+
*"some@example.com",
100+
Keys.ENTER,
101+
Keys.ENTER,
102+
# Name
103+
*"Patrick",
104+
Keys.TAB,
105+
# Organization
106+
*"FastAPI Cloud",
107+
Keys.TAB,
108+
# Team
109+
*"Team A",
110+
Keys.TAB,
111+
# Role
112+
*"Developer",
113+
Keys.TAB,
114+
# Location
115+
*"London",
116+
Keys.TAB,
117+
# Use case
118+
*"I want to build a web app",
119+
Keys.TAB,
120+
# Secret code
121+
*"PyCon Italia",
122+
Keys.ENTER,
123+
Keys.ENTER,
124+
]
125+
126+
respx_mock.post(
127+
"/users/waiting-list",
128+
json={
129+
"email": "some@example.com",
130+
"name": "Patrick",
131+
"organization": "FastAPI Cloud",
132+
"role": "Developer",
133+
"team_size": None,
134+
"location": "London",
135+
"use_case": "I want to build a web app",
136+
"secret_code": "PyCon Italia",
137+
},
138+
).mock(return_value=Response(200))
139+
140+
with changing_dir(tmp_path), patch(
141+
"rich_toolkit.menu.click.getchar"
142+
) as mock_getchar:
143+
mock_getchar.side_effect = steps
144+
145+
result = runner.invoke(app, ["deploy"])
63146

64147
assert result.exit_code == 1
65-
assert "No credentials found." in result.output
148+
assert "We're currently in private beta" in result.output
149+
assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output
66150

67151

68152
def test_asks_to_setup_the_app(logged_in_cli: None, tmp_path: Path) -> None:

tests/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ class Keys:
1818
RIGHT_ARROW = "\x1b[C"
1919
ENTER = "\r"
2020
CTRL_C = "\x03"
21+
TAB = "\t"

0 commit comments

Comments
 (0)