python-fasthtml 0.12.5__tar.gz → 0.12.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_fasthtml-0.12.5/python_fasthtml.egg-info → python_fasthtml-0.12.6}/PKG-INFO +1 -1
- python_fasthtml-0.12.6/fasthtml/__init__.py +2 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/_modidx.py +14 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/core.py +1 -1
- python_fasthtml-0.12.6/fasthtml/stripe_otp.py +212 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6/python_fasthtml.egg-info}/PKG-INFO +1 -1
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/SOURCES.txt +1 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/settings.ini +1 -1
- python_fasthtml-0.12.5/fasthtml/__init__.py +0 -2
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/CONTRIBUTING.md +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/LICENSE +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/MANIFEST.in +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/README.md +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/basics.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/cli.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/common.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/components.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/components.pyi +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/core.pyi +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/fastapp.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/ft.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/js.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/jupyter.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/katex.js +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/oauth.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/pico.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/svg.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/xtend.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/fasthtml/xtend.pyi +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/pyproject.toml +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/not-zip-safe +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/requires.txt +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/setup.cfg +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/setup.py +0 -0
- {python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/tests/test_toaster.py +0 -0
|
@@ -201,6 +201,20 @@ d = { 'settings': { 'branch': 'main',
|
|
|
201
201
|
'fasthtml.pico.Search': ('api/pico.html#search', 'fasthtml/pico.py'),
|
|
202
202
|
'fasthtml.pico.set_pico_cls': ('api/pico.html#set_pico_cls', 'fasthtml/pico.py')},
|
|
203
203
|
'fasthtml.starlette': {},
|
|
204
|
+
'fasthtml.stripe_otp': { 'fasthtml.stripe_otp.Payment': ('explains/stripe.html#payment', 'fasthtml/stripe_otp.py'),
|
|
205
|
+
'fasthtml.stripe_otp._search_app': ('explains/stripe.html#_search_app', 'fasthtml/stripe_otp.py'),
|
|
206
|
+
'fasthtml.stripe_otp.account_management': ( 'explains/stripe.html#account_management',
|
|
207
|
+
'fasthtml/stripe_otp.py'),
|
|
208
|
+
'fasthtml.stripe_otp.archive_price': ('explains/stripe.html#archive_price', 'fasthtml/stripe_otp.py'),
|
|
209
|
+
'fasthtml.stripe_otp.before': ('explains/stripe.html#before', 'fasthtml/stripe_otp.py'),
|
|
210
|
+
'fasthtml.stripe_otp.cancel': ('explains/stripe.html#cancel', 'fasthtml/stripe_otp.py'),
|
|
211
|
+
'fasthtml.stripe_otp.create_checkout_session': ( 'explains/stripe.html#create_checkout_session',
|
|
212
|
+
'fasthtml/stripe_otp.py'),
|
|
213
|
+
'fasthtml.stripe_otp.create_price': ('explains/stripe.html#create_price', 'fasthtml/stripe_otp.py'),
|
|
214
|
+
'fasthtml.stripe_otp.home': ('explains/stripe.html#home', 'fasthtml/stripe_otp.py'),
|
|
215
|
+
'fasthtml.stripe_otp.post': ('explains/stripe.html#post', 'fasthtml/stripe_otp.py'),
|
|
216
|
+
'fasthtml.stripe_otp.refund': ('explains/stripe.html#refund', 'fasthtml/stripe_otp.py'),
|
|
217
|
+
'fasthtml.stripe_otp.success': ('explains/stripe.html#success', 'fasthtml/stripe_otp.py')},
|
|
204
218
|
'fasthtml.svg': { 'fasthtml.svg.Circle': ('api/svg.html#circle', 'fasthtml/svg.py'),
|
|
205
219
|
'fasthtml.svg.Ellipse': ('api/svg.html#ellipse', 'fasthtml/svg.py'),
|
|
206
220
|
'fasthtml.svg.Line': ('api/svg.html#line', 'fasthtml/svg.py'),
|
|
@@ -131,7 +131,7 @@ def _is_body(anno): return issubclass(anno, (dict,ns)) or _annotations(anno)
|
|
|
131
131
|
# %% ../nbs/api/00_core.ipynb
|
|
132
132
|
def _formitem(form, k):
|
|
133
133
|
"Return single item `k` from `form` if len 1, otherwise return list"
|
|
134
|
-
if isinstance(form, dict): return form
|
|
134
|
+
if isinstance(form, dict): return form.get(k)
|
|
135
135
|
o = form.getlist(k)
|
|
136
136
|
return o[0] if len(o) == 1 else o if o else None
|
|
137
137
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/explains/Stripe.ipynb.
|
|
2
|
+
|
|
3
|
+
# %% auto 0
|
|
4
|
+
__all__ = ['DOMAIN_URL', 'app_nm', 'price_list', 'price', 'bware', 'app', 'rt', 'WEBHOOK_SECRET', 'db', 'payments',
|
|
5
|
+
'create_price', 'archive_price', 'before', 'home', 'create_checkout_session', 'Payment', 'post', 'success',
|
|
6
|
+
'cancel', 'refund', 'account_management']
|
|
7
|
+
|
|
8
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
9
|
+
from .common import *
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
13
|
+
import stripe
|
|
14
|
+
|
|
15
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
16
|
+
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY")
|
|
17
|
+
DOMAIN_URL = os.environ.get("DOMAIN_URL", "http://localhost:5001")
|
|
18
|
+
|
|
19
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
20
|
+
def _search_app(app_nm:str, limit=1):
|
|
21
|
+
"Checks for product based on app_nm and returns the product if it exists"
|
|
22
|
+
return stripe.Product.search(query=f"name:'{app_nm}' AND active:'True'", limit=limit).data
|
|
23
|
+
|
|
24
|
+
def create_price(app_nm:str, amt:int, currency="usd") -> list[stripe.Price]:
|
|
25
|
+
"Create a product and bind it to a price object. If product already exist just return the price list."
|
|
26
|
+
existing_product = _search_app(app_nm)
|
|
27
|
+
if existing_product:
|
|
28
|
+
return stripe.Price.list(product=existing_product[0].id).data
|
|
29
|
+
else:
|
|
30
|
+
product = stripe.Product.create(name=f"{app_nm}")
|
|
31
|
+
return [stripe.Price.create(product=product.id, unit_amount=amt, currency=currency)]
|
|
32
|
+
|
|
33
|
+
def archive_price(app_nm:str):
|
|
34
|
+
"Archive a price - useful for cleanup if testing."
|
|
35
|
+
existing_products = _search_app(app_nm, limit=50)
|
|
36
|
+
for product in existing_products:
|
|
37
|
+
for price in stripe.Price.list(product=product.id).data:
|
|
38
|
+
stripe.Price.modify(price.id, active=False)
|
|
39
|
+
stripe.Product.modify(product.id, active=False)
|
|
40
|
+
|
|
41
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
42
|
+
app_nm = "[FastHTML Docs] Demo Product"
|
|
43
|
+
price_list = create_price(app_nm, amt=1999)
|
|
44
|
+
assert len(price_list) == 1, 'For this tutorial, we only have one price bound to our product.'
|
|
45
|
+
price = price_list[0]
|
|
46
|
+
|
|
47
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
48
|
+
def before(sess): sess['auth'] = 'hamel@hamel.com'
|
|
49
|
+
bware = Beforeware(before, skip=['/webhook'])
|
|
50
|
+
app, rt = fast_app(before=bware)
|
|
51
|
+
|
|
52
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
53
|
+
WEBHOOK_SECRET = os.getenv("STRIPE_LOCAL_TEST_WEBHOOK_SECRET")
|
|
54
|
+
|
|
55
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
56
|
+
@rt("/")
|
|
57
|
+
def home(sess):
|
|
58
|
+
auth = sess['auth']
|
|
59
|
+
return Titled(
|
|
60
|
+
"Buy Now",
|
|
61
|
+
Div(H2("Demo Product - $19.99"),
|
|
62
|
+
P(f"Welcome, {auth}"),
|
|
63
|
+
Button("Buy Now", hx_post="/create-checkout-session", hx_swap="none"),
|
|
64
|
+
A("View Account", href="/account")))
|
|
65
|
+
|
|
66
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
67
|
+
@rt("/create-checkout-session", methods=["POST"])
|
|
68
|
+
async def create_checkout_session(sess):
|
|
69
|
+
checkout_session = stripe.checkout.Session.create(
|
|
70
|
+
line_items=[{'price': price.id, 'quantity': 1}],
|
|
71
|
+
mode='payment',
|
|
72
|
+
payment_method_types=['card'],
|
|
73
|
+
customer_email=sess['auth'],
|
|
74
|
+
metadata={'app_name': app_nm,
|
|
75
|
+
'AnyOther': 'Metadata',},
|
|
76
|
+
# CHECKOUT_SESSION_ID is a special variable Stripe fills in for you
|
|
77
|
+
success_url=DOMAIN_URL + '/success?checkout_sid={CHECKOUT_SESSION_ID}',
|
|
78
|
+
cancel_url=DOMAIN_URL + '/cancel')
|
|
79
|
+
return Redirect(checkout_session.url)
|
|
80
|
+
|
|
81
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
82
|
+
# Database Table
|
|
83
|
+
class Payment:
|
|
84
|
+
checkout_session_id: str # Stripe checkout session ID (primary key)
|
|
85
|
+
email: str
|
|
86
|
+
amount: int # Amount paid in cents
|
|
87
|
+
payment_status: str # paid, pending, failed
|
|
88
|
+
created_at: int # Unix timestamp
|
|
89
|
+
metadata: str # Additional payment metadata as JSON
|
|
90
|
+
|
|
91
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
92
|
+
db = Database("stripe_payments.db")
|
|
93
|
+
payments = db.create(Payment, pk='checkout_session_id', transform=True)
|
|
94
|
+
|
|
95
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
96
|
+
@rt("/webhook")
|
|
97
|
+
async def post(req):
|
|
98
|
+
payload = await req.body()
|
|
99
|
+
# Verify the event came from Stripe
|
|
100
|
+
try:
|
|
101
|
+
event = stripe.Webhook.construct_event(
|
|
102
|
+
payload, req.headers.get("stripe-signature"), WEBHOOK_SECRET)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Webhook error: {e}")
|
|
105
|
+
return
|
|
106
|
+
if event and event.type == "checkout.session.completed":
|
|
107
|
+
event_data = event.data.object
|
|
108
|
+
if event_data.metadata.get('app_name') == app_nm:
|
|
109
|
+
payment = Payment(
|
|
110
|
+
checkout_session_id=event_data.id,
|
|
111
|
+
email=event_data.customer_email,
|
|
112
|
+
amount=event_data.amount_total,
|
|
113
|
+
payment_status=event_data.payment_status,
|
|
114
|
+
created_at=event_data.created,
|
|
115
|
+
metadata=str(event_data.metadata))
|
|
116
|
+
payments.insert(payment)
|
|
117
|
+
print(f"Payment recorded for user: {event_data.customer_email}")
|
|
118
|
+
|
|
119
|
+
# Do not worry about refunds yet, we will cover how to do this later in the tutorial
|
|
120
|
+
elif event and event.type == "charge.refunded":
|
|
121
|
+
event_data = event.data.object
|
|
122
|
+
payment_intent_id = event_data.payment_intent
|
|
123
|
+
sessions = stripe.checkout.Session.list(payment_intent=payment_intent_id)
|
|
124
|
+
if sessions and sessions.data:
|
|
125
|
+
checkout_sid = sessions.data[0].id
|
|
126
|
+
payments.update(Payment(checkout_session_id= checkout_sid, payment_status="refunded"))
|
|
127
|
+
print(f"Refund recorded for payment: {checkout_sid}")
|
|
128
|
+
|
|
129
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
130
|
+
@rt("/success")
|
|
131
|
+
def success(sess, checkout_sid:str):
|
|
132
|
+
# Get payment record from database (saved in the webhook)
|
|
133
|
+
payment = payments[checkout_sid]
|
|
134
|
+
|
|
135
|
+
if not payment or payment.payment_status != 'paid':
|
|
136
|
+
return Titled("Error", P("Payment not found"))
|
|
137
|
+
|
|
138
|
+
return Titled(
|
|
139
|
+
"Success",
|
|
140
|
+
Div(H2("Payment Successful!"),
|
|
141
|
+
P(f"Thank you for your purchase, {sess['auth']}"),
|
|
142
|
+
P(f"Amount Paid: ${payment.amount / 100:.2f}"),
|
|
143
|
+
P(f"Status: {payment.payment_status}"),
|
|
144
|
+
P(f"Transaction ID: {payment.checkout_session_id}"),
|
|
145
|
+
A("Back to Home", href="/")))
|
|
146
|
+
|
|
147
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
148
|
+
@rt("/cancel")
|
|
149
|
+
def cancel():
|
|
150
|
+
return Titled(
|
|
151
|
+
"Cancelled",
|
|
152
|
+
Div(H2("Payment Cancelled"),
|
|
153
|
+
P("Your payment was cancelled."),
|
|
154
|
+
A("Back to Home", href="/")))
|
|
155
|
+
|
|
156
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
157
|
+
@rt("/refund", methods=["POST"])
|
|
158
|
+
async def refund(sess, checkout_sid:str):
|
|
159
|
+
# Get payment record from database
|
|
160
|
+
payment = payments[checkout_sid]
|
|
161
|
+
|
|
162
|
+
if not payment or payment.payment_status != 'paid':
|
|
163
|
+
return P("Error: Payment not found or not eligible for refund")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Get the payment intent ID from the checkout session
|
|
167
|
+
checkout_session = stripe.checkout.Session.retrieve(checkout_sid)
|
|
168
|
+
|
|
169
|
+
# Process the refund
|
|
170
|
+
refund = stripe.Refund.create(payment_intent=checkout_session.payment_intent, reason="requested_by_customer")
|
|
171
|
+
|
|
172
|
+
# Update payment status in database
|
|
173
|
+
payments.update(Payment(checkout_session_id= checkout_sid, payment_status="refunded"))
|
|
174
|
+
|
|
175
|
+
return Div(
|
|
176
|
+
P("Refund processed successfully!"),
|
|
177
|
+
P(f"Refund ID: {refund.id}"))
|
|
178
|
+
|
|
179
|
+
except Exception as e: return P(f"Refund failed: {str(e)}")
|
|
180
|
+
|
|
181
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
182
|
+
@rt("/account")
|
|
183
|
+
def account_management(sess):
|
|
184
|
+
user_email = sess['auth']
|
|
185
|
+
user_payments = payments("email=?", (user_email,))
|
|
186
|
+
# Create table rows for each payment
|
|
187
|
+
payment_rows = []
|
|
188
|
+
for payment in user_payments:
|
|
189
|
+
action_button = ""
|
|
190
|
+
if payment.payment_status == 'paid':
|
|
191
|
+
action_button = Button("Request Refund", hx_post=f"/refund?checkout_sid={payment.checkout_session_id}",hx_target="#refund-status")
|
|
192
|
+
elif payment.payment_status == 'refunded': action_button = "Refunded"
|
|
193
|
+
|
|
194
|
+
# Add row to table
|
|
195
|
+
payment_rows.append(
|
|
196
|
+
Tr(*map(Td, (payment.created_at, payment.amount, payment.payment_status, action_button))))
|
|
197
|
+
|
|
198
|
+
# Create payment history table
|
|
199
|
+
payment_history = Table(
|
|
200
|
+
Thead(Tr(*map(Th, ("Date", "Amount", "Status", "Action")))),
|
|
201
|
+
Tbody(*payment_rows))
|
|
202
|
+
|
|
203
|
+
return Titled(
|
|
204
|
+
"Account Management",
|
|
205
|
+
Div(H2(f"Account: {user_email}"),
|
|
206
|
+
H3("Payment History"),
|
|
207
|
+
payment_history,
|
|
208
|
+
Div(id="refund-status"), # Target for refund status messages
|
|
209
|
+
A("Back to Home", href="/")))
|
|
210
|
+
|
|
211
|
+
# %% ../nbs/explains/Stripe.ipynb
|
|
212
|
+
serve()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[DEFAULT]
|
|
2
2
|
repo = fasthtml
|
|
3
3
|
lib_name = fasthtml
|
|
4
|
-
version = 0.12.
|
|
4
|
+
version = 0.12.6
|
|
5
5
|
min_python = 3.10
|
|
6
6
|
license = apache2
|
|
7
7
|
requirements = fastcore>=1.7.18 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.1.1 python-multipart beautifulsoup4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fasthtml-0.12.5 → python_fasthtml-0.12.6}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|