Skip to content

Commit 776710f

Browse files
authored
Build out and document facets in python (#446)
2 parents 6b3e41f + 65694ab commit 776710f

File tree

19 files changed

+894
-226
lines changed

19 files changed

+894
-226
lines changed

Diff for: .github/workflows/python-ci.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ jobs:
4848
- name: example-pytest-selfie - poetry install
4949
run: poetry install
5050
working-directory: python/example-pytest-selfie
51-
# - run: poetry run pytest -vv
52-
# working-directory: python/example-pytest-selfie
53-
# - name: example-pytest-selfie - pyright
54-
# run: poetry run pyright
55-
# working-directory: python/example-pytest-selfie
51+
- run: poetry run pytest -vv
52+
working-directory: python/example-pytest-selfie
53+
- name: example-pytest-selfie - pyright
54+
run: poetry run pyright
55+
working-directory: python/example-pytest-selfie
5656
- name: example-pytest-selfie - ruff
5757
run: poetry run ruff format --check && poetry run ruff check
5858
working-directory: python/example-pytest-selfie

Diff for: python/.python-version

-1
This file was deleted.

Diff for: python/example-pytest-selfie/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The purpose of this project is to demonstrate selfie for the manual.
2+
3+
Go to https://selfie.dev/py/facets for the tutorial.
4+
5+
- First run `poetry install`
6+
- You can run the app locally with `poetry run python app.py`
7+
- You can run the tests with `poetry run pytest`

Diff for: python/example-pytest-selfie/app.py

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# app.py
2+
import base64
3+
import hashlib
4+
import threading
5+
import time
6+
from datetime import datetime, timedelta
7+
from functools import wraps
8+
from random import Random
9+
10+
from flask import (
11+
Flask,
12+
jsonify,
13+
make_response,
14+
redirect,
15+
render_template_string,
16+
request,
17+
)
18+
19+
random_0 = Random(0)
20+
app = Flask(__name__)
21+
22+
# In-memory database (replace with a real database in production)
23+
database = {}
24+
25+
# Email storage for development
26+
email_storage = []
27+
email_lock = threading.Lock()
28+
email_condition = threading.Condition(email_lock)
29+
30+
31+
class DevTime:
32+
def __init__(self):
33+
self.current_time = datetime(2000, 1, 1)
34+
35+
def set_year(self, year):
36+
self.current_time = datetime(year, 1, 1)
37+
38+
def advance_24hrs(self):
39+
self.current_time += timedelta(days=1)
40+
41+
def now(self):
42+
return self.current_time
43+
44+
45+
dev_time = DevTime()
46+
47+
48+
def repeatable_random(length):
49+
# This is a simplified version, not as secure as Java's SecureRandom
50+
return "".join(
51+
random_0.choice(
52+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
53+
)
54+
for _ in range(length)
55+
)
56+
57+
58+
def send_email(to_email, subject, html_content):
59+
email = {"to": to_email, "subject": subject, "html_content": html_content}
60+
with email_lock:
61+
email_storage.append(email)
62+
email_condition.notify_all()
63+
64+
65+
def sign_email(email):
66+
terrible_security = "password"
67+
return base64.urlsafe_b64encode(
68+
hashlib.sha256(f"{email}{terrible_security}".encode()).digest()
69+
).decode()
70+
71+
72+
def auth_required(f):
73+
@wraps(f)
74+
def decorated_function(*args, **kwargs):
75+
user = auth_user()
76+
if user is None:
77+
return redirect("/")
78+
return f(user, *args, **kwargs)
79+
80+
return decorated_function
81+
82+
83+
def auth_user():
84+
login_cookie = request.cookies.get("login")
85+
if not login_cookie:
86+
return None
87+
email, signature = login_cookie.split("|")
88+
if signature != sign_email(email):
89+
return None
90+
return {"email": email}
91+
92+
93+
@app.route("/")
94+
def index():
95+
user = auth_user()
96+
if user:
97+
return render_template_string(
98+
"""
99+
<html><body>
100+
<h1>Welcome back {{ username }}</h1>
101+
</body></html>""",
102+
username=user["email"],
103+
)
104+
else:
105+
return render_template_string(
106+
"""
107+
<html><body>
108+
<h1>Please login</h1>
109+
<form action="/login" method="post">
110+
<input type="text" name="email" placeholder="email">
111+
<input type="submit" value="login">
112+
</form>
113+
</body></html>"""
114+
)
115+
116+
117+
@app.route("/login", methods=["POST"])
118+
def login():
119+
email = request.form["email"]
120+
random_code = repeatable_random(7)
121+
database[random_code] = email
122+
123+
login_link = f"http://{request.host}/login-confirm/{random_code}"
124+
send_email(
125+
email,
126+
"Login to example.com",
127+
f'Click <a href="{login_link}">here</a> to login.',
128+
)
129+
130+
return render_template_string(
131+
"""
132+
<html><body>
133+
<h1>Email sent!</h1>
134+
<p>Check your email for your login link.</p>
135+
</body></html>"""
136+
)
137+
138+
139+
@app.route("/login-confirm/<code>")
140+
def login_confirm(code):
141+
email = database.pop(code, None)
142+
if email is None:
143+
return render_template_string(
144+
"""
145+
<html><body>
146+
<h1>Login link expired.</h1>
147+
<p>Sorry, <a href="/">try again</a>.</p>
148+
</body></html>"""
149+
)
150+
response = make_response(redirect("/"))
151+
response.set_cookie("login", f"{email}|{sign_email(email)}")
152+
return response
153+
154+
155+
@app.route("/email")
156+
def email_list():
157+
messages = email_storage
158+
html = "<h2>Messages</h2><ul>"
159+
if not messages:
160+
html += "<li>(none)</li>"
161+
else:
162+
for i, message in enumerate(messages, 1):
163+
html += f'<li><a href="/email/message/{i}">{i}: {message["to"]} {message["subject"]}</a></li>'
164+
html += "</ul>"
165+
return html
166+
167+
168+
@app.route("/email/message/<int:idx>")
169+
def email_message(idx):
170+
idx -= 1
171+
if 0 <= idx < len(email_storage):
172+
return email_storage[idx]["html_content"]
173+
else:
174+
return "No such message"
175+
176+
177+
def wait_for_incoming_email(timeout=1):
178+
start_time = time.time()
179+
with email_lock:
180+
while len(email_storage) == 0:
181+
remaining_time = timeout - (time.time() - start_time)
182+
if remaining_time <= 0:
183+
raise TimeoutError("Email wasn't sent within the specified timeout")
184+
email_condition.wait(timeout=remaining_time)
185+
return email_storage[-1]
186+
187+
188+
@app.route("/dev/time", methods=["POST"])
189+
def set_dev_time():
190+
json = request.json
191+
assert json is not None
192+
action = json.get("action")
193+
if action == "set_year":
194+
year = json.get("year")
195+
dev_time.set_year(year)
196+
elif action == "advance_24hrs":
197+
dev_time.advance_24hrs()
198+
return jsonify({"current_time": dev_time.now().isoformat()})
199+
200+
201+
if __name__ == "__main__":
202+
print("Opening selfie demo app at http://localhost:5000")
203+
app.run(debug=True)

Diff for: python/example-pytest-selfie/example_pytest_selfie/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)