Skip to content

Commit 516d9b5

Browse files
committed
✨ Add waitlist form
1 parent a1731b5 commit 516d9b5

4 files changed

Lines changed: 208 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: Optional[str] = None
393+
organization: Optional[str] = None
394+
role: Optional[str] = None
395+
team_size: Optional[str] = None
396+
location: Optional[str] = None
397+
use_case: Optional[str] = None
398+
secret_code: Optional[str] = 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:
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: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,94 @@ def _get_random_deployment(
5858
}
5959

6060

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

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

67150

68151
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)