Published

All published posts

2717 posts latest post 2026-11-11
Publishing rhythm
Oct 2026 | 30 posts
waylon walker (@_WaylonWalker) on X Bloggers where do you put your markdown? X (formerly Twitter) · twitter.com Most bloggers on my twitter blog right into a file that goes on git. I kinda expected to have more database folk. I have my blog in markdown on git and the editing experience is top notch. I can just find files edit them in MY EDITOR, push them and I got a post. I am running thoughts in a sqlite database with a fastapi backend, and holy crap the instant nature of posting feels so much better. Both sides have good points.
Show some equivalent list comprehensions in filter examples · Issue #1068 · pallets/jinja I'm willing to write a pull-request for this, but I just want to see what people think before I write it. So the issue is this. I'm very familiar with python. I'm new to Jinja2. Often I find myself... GitHub · github.com I often want to reach for non existing list comprehensions in jinja 2, Here are a few nice equivalents.

I am working on fokais.com’s signup page, and I want to hide the form input during an htmx request. I was seeing some issues where I was able to prevent spamming the submit button, but was still able to get one extra hit on it.

It also felt like nothing was happening while sending the email to the user for verification. Now I get the form to disappear and a spinner to show during the request.

HTML #

Let’s start off with the form. It uses htmx to submit a post request to the post_request route. Note that there is a spinner in the post_request with the htmx-indicator class.

The intent is to hide the spinner until the request is running, and hide all of the form input during the request.

<form
  id="signup-form"
  hx-swap-oob="outerHTML"
  class="m-4 mx-auto mb-6 flex w-80 flex-col rounded-lg b p-4 shadow-xlc shadow-cyan-500/10"
  method="POST"
  action="{{ url_for('post_signup') }}"
  hx-post="{{ url_for('post_signup') }}"
>

<!--markata-attribution-->
  <input
    class="mx-8 mt-6 mb-4 border border-black bg-zinc-900 p-1 text-center focus:bg-zinc-800"

<!--markata-attribution-->
    type="text"

<!--markata-attribution-->
    value="{{ full_name }}"

<!--markata-attribution-->
    name="full_name"

<!--markata-attribution-->
    placeholder="Full Name"
  />

<!--markata-attribution-->
  {% if full_name_error %}

<!--markata-attribution-->
  <label class="-mt-6 mb-6 mx-8 text-red-500 p-1 text-center">

<!--markata-attribution-->
    {{ full_name_error }}

<!--markata-attribution-->
  </label>

<!--markata-attribution-->
  {% endif %}

<!--markata-attribution-->
  <input
    class="mx-8 mb-4 border border-black bg-zinc-900 p-1 text-center focus:bg-zinc-800"

<!--markata-attribution-->
    type="text"

<!--markata-attribution-->
    value="{{ username }}"

<!--markata-attribution-->
    name="username"

<!--markata-attribution-->
    placeholder="username"
  />

<!--markata-attribution-->
  {% if username_error %}

<!--markata-attribution-->
  <label class="-mt-6 mb-6 mx-8 text-red-500 p-1 text-center">

<!--markata-attribution-->
    {{ username_error }}

<!--markata-attribution-->
  </label>

<!--markata-attribution-->
  {% endif %}

<!--markata-attribution-->
  <input
    class="mx-8 mb-4 border border-black bg-zinc-900 p-1 text-center focus:bg-zinc-800"

<!--markata-attribution-->
    type="email"

<!--markata-attribution-->
    name="email"

<!--markata-attribution-->
    value="{{ email }}"

<!--markata-attribution-->
    placeholder="email"
  />

<!--markata-attribution-->
  {% if email_error %}

<!--markata-attribution-->
  <label class="-mt-6 mb-6 mx-8 text-red-500 p-1 text-center">

<!--markata-attribution-->
    {{ email_error }}

<!--markata-attribution-->
  </label>

<!--markata-attribution-->
  {% endif %}

<!--markata-attribution-->
  <input
    class="mx-auto w-32 mb-4 border border-black bg-purple-900 p-1 text-center focus:bg-zinc-800"

<!--markata-attribution-->
    type="submit"

<!--markata-attribution-->
    value="sign up"
  />

<!--markata-attribution-->
  <div role="status" class="mx-auto htmx-indicator">

<!--markata-attribution-->
    <svg

<!--markata-attribution-->
      class="mx-auto animate-spin h-5 w-5 text-white"

<!--markata-attribution-->
      xmlns="https://www.w3.org/2000/svg"

<!--markata-attribution-->
      fill="none"

<!--markata-attribution-->
      viewBox="0 0 24 24"
    >

<!--markata-attribution-->
      <circle

<!--markata-attribution-->
        class="opacity-25"

<!--markata-attribution-->
        cx="12"

<!--markata-attribution-->
        cy="12"

<!--markata-attribution-->
        r="10"

<!--markata-attribution-->
        stroke="currentColor"

<!--markata-attribution-->
        stroke-width="4"
      ></circle>

<!--markata-attribution-->
      <path

<!--markata-attribution-->
        class="opacity-75"

<!--markata-attribution-->
        fill="currentColor"
        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
      ></path>

<!--markata-attribution-->
    </svg>

<!--markata-attribution-->
    <p>Signing up...</p>

<!--markata-attribution-->
  </div>

<!--markata-attribution-->
</form>

Yes this is styled using tailwindcss.

https://waylonwalker.com/still-loving-tailwind/

CSS #

Let’s take a look at how we achieve switching between only spinner an only form inputs using css.

.htmx-indicator {
  @apply hidden;
  opacity: 0;
  transition: opacity 500ms ease-in;
}
.htmx-request button,
.htmx-request input[type="submit"],
.htmx-request input,
.htmx-request label {
  @apply hidden;
}
.htmx-request .htmx-indicator {
  opacity: 1;
  @apply block;
}
.htmx-request.htmx-indicator {
  opacity: 1;
  @apply block;
}

Final Result #

Here is the final result of me signing up for a new account in fokais.

GitHub - DataDog/ddqa: Datadog's QA manager for releases of GitHub repositories Datadog's QA manager for releases of GitHub repositories - DataDog/ddqa GitHub · github.com DataDog ddqa is building out a textual app and deploying it with pyapp. They have CI setup to fully build and cross compile their textual tui into github releases that you can just download from their releases page. This is something I am looking at for markata. This would be pretty sweet to be able to make it just work on places like windows. It would also be interesting to try to build a full desktop app with pyapp.
Check out ddqa by DataDog. It’s a well-crafted project with great potential. Datadog’s QA manager for releases of GitHub repositories
I like cross-rs’s project cross. “Zero setup” cross compilation and “cross testing” of Rust crates
If you’re into interesting projects, don’t miss out on pyapp, created by ofek. Runtime installer for Python applications
Adam Wathan (@adamwathan) on X Hear me out. X (formerly Twitter) · twitter.com I’m going to give this trick a shot on my sites, and see how I like it. Down in the comments @adamwathan goes on to say. Basically every layout overflow bug ever boils down to some flex or grid child needing min-width: 0 😄 Oh and @ryanflorence also says in the comments. I … do this.
External Link tushar.lol Nice message by @tusharsadhwani. Write it down. You had to dig deeper than face value at something. Write it down. You had to combine multiple pages of docs. Write it down. Someting was simply not obvious to you at first and it took someone else to give you that ah ha moment. Write it down. You had a small discovery that had a marginal impact on your day. Write it down. A blog does not have to be a Blog, it can be small meaningful posts. There are absolutely no rules. If you think you are going to end up with too many posts, that is a solvable problem, make a search, curate your favorite posts, make multiple feeds. At the end of the day. Write it down. This post itself is a thought, the smallest component to my blogging strategy. Write it down.
Path Operation Advanced Configuration - FastAPI FastAPI framework, high performance, easy to learn, fast to code, ready for production fastapi.tiangolo.com From the Docs trailing slash I’ve had better luck just routing both naked and trailing slash routes in fastapi. I’ve had api’s deployed as a subroute to a site rather than a subdomain, and the automatic redirect betweens them tended to always get messed up. This is pretty easy fix for the pain is causes just give vim a yyp, and if you don’t want deuplicates in your docs, ignore one. favicon.ico Now you do not need to deploy favicons to your api in any way, it is nice to have it in your browser tab, but more importantly to me I hate having console errors that are meaningless, this gives the browser something to automatically grab and not log errors.
Protect API docs behind authentication? · Issue #364 · fastapi/fastapi Basic Question Does FastAPI provide a method for implementing authentication middleware or similar on the docs themselves (e.g. to protect access to /docs and /redoc)? Additional context My company... GitHub · github.com You can protect your fastapi docs behind auth so that not only can certain roles not run certain routes, but they cannot even see the docs at all. This way no one that shouldn’t be poking around can even discover routes they shouldn’t be using. Here is the soluteion provided by @kennylajara
Looking for inspiration? llmware by llmware-ai. Unified framework for building enterprise RAG pipelines with small, specialized models
Cancel subscriptions Cancel subscriptions immediately or at the end of the subscription period with proration options, invoice handling, and automatic cancellation after failed payment attempts. stripe.com This is a handy guide to cancelling stripe subscriptions. You can even inverse it by flipping to and re activate the subscription.

Today I am working on fokais.com, trying to get to a point where I can launch by workig through stripe integrations. This is my first time using stripe, so there has been quite a bit to learn, and I am probably building in more than I need to before launching, but I am learning, and not in a rush to launch.

I am building the fokais backent in python primarilyt with fastapi and sqlmodel on sqlite. My billing integration is going to be all Stripe.

Stripe Subscription Cancellations Docs #

Here is a link to the stripe docs for your refrence, especially if you want to see how to cancel subscriptions in other languages. They include code samples for many popular languages.

Cancel subscriptions | Stripe Documentation

User Model #

This is the part of the user model that includes the cancel and reactivate methods. It pretty much follows the stripe guide.

class UserBase(SQLModel, table=False):  # type: ignore[call-arg]
    username: str = Field(unique=True)
    full_name: str
    email: str
    email_verified: bool = False
    disabled: bool = False
    signup_date: Optional[datetime] = Field(default_factory=datetime.utcnow)
    stripe_customer_id: Optional[str]

    def cancel_subscription(self):
        for subscription in self.active_subscriptions:
            stripe.Subscription.modify(
                subscription.id,
                cancel_at_period_end=True,
            )
        self.refresh()

    def reactivate_subscription(self):
        for subscription in self.active_subscriptions:
            stripe.Subscription.modify(
                subscription.id,
                cancel_at_period_end=False,
            )
        self.refresh()

Cancellations api #

Here is the cancellations api. I created an are you sure form that I can link to from the accounts page with a normal anchor tag. Note that I am doing a POST request to do the cancellation from a form. I want this to work for any user whether there is js or not. This is an operation that will change the users data, and I want to make sure that it avoids all browser and cdn caching. As a scrappy startup we are running light on infrastructure and are caching hard at the CDN to avoid excessive server hits.

Note

I am doing a POST request to do the cancellation from a form.

@pricing_router.get("/cancel")
@pricing_router.get("/cancel/")
def get_cancel(
    request: Request,
    current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
    return config.templates.TemplateResponse(
        "cancel.html",
        {
            "request": request,
            "prices": products.prices,
            "products": products.products,
            "current_user": current_user,
        },
    )


@pricing_router.post("/cancel")
@pricing_router.post("/cancel/")
def post_cancel(
    request: Request,
    current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
    current_user.cancel_subscription()
    return HTMLResponse('<p id="cancel" hx-swap-oob="outerHTML">Your Subscription has been Cancelled</p>')

Reactivations #

Reactivating accounts looks just about the same as cancelling, only flippng True to False.


@pricing_router.get("/reactivate")
@pricing_router.get("/reactivate/")
def get_reactivate(
    request: Request,
    current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
    return config.templates.TemplateResponse(
        "reactivate.html",
        {
            "request": request,
            "prices": products.prices,
            "products": products.products,
            "current_user": current_user,
        },
    )


@pricing_router.post("/reactivate")
@pricing_router.post("/reactivate/")
def post_reactivate(
    request: Request,
    current_user: Annotated[User, Depends(get_current_user_if_logged_in)],
):
    current_user.reactivate_subscription()
    return HTMLResponse('<p id="reactivate" hx-swap-oob="outerHTML">Your Subscription has been reactivated</p>')

Full User Model #

This is the full user model, completely subject to change in the future, but it includes the cancel and reactivate methods.

class UserBase(SQLModel, table=False):  # type: ignore[call-arg]
    username: str = Field(unique=True)
    full_name: str
    email: str
    email_verified: bool = False
    disabled: bool = False
    signup_date: Optional[datetime] = Field(default_factory=datetime.utcnow)
    stripe_customer_id: Optional[str]

    @property
    def session(self):
        return next(get_session())

    @classmethod
    def get_by_id(cls, id):
        return next(get_session()).get(cls, id)

    def refresh(self):
        cache.set(f"active_subscriptions_{self.id}", None, 3600)
        cache.set(f"active_products_{self.id}", None, 3600)

    def get_checkout_sessions(self):
        return [
            stripe.checkout.Session.retrieve(s.stripe_checkout_session_id)
            for s in self.session.exec(select(CheckoutSession).where(CheckoutSession.user_id == self.id)).all()
            if s.stripe_checkout_session_id is not None
        ]

    def get_active_subscriptions(self):
        subscriptions = [
            s.subscription
            for s in [
                stripe.checkout.Session.retrieve(s.stripe_checkout_session_id)
                for s in self.session.exec(select(CheckoutSession).where(CheckoutSession.user_id == self.id)).all()
                if s.stripe_checkout_session_id is not None
            ]
            if s.status == "complete"
        ]
        active_subscriptions = [stripe.Subscription.retrieve(subscription) for subscription in subscriptions]
        return active_subscriptions

    def has_active_subscription(self):
        return len(self.active_subscriptions) > 0

    @property
    def active_subscriptions(self):
        active_subscriptions = cache.get(f"active_subscriptions_{self.id}")
        if active_subscriptions is not None:
            return active_subscriptions
        active_subscriptions = self.get_active_subscriptions()
        cache.set(f"active_subscriptions_{self.id}", active_subscriptions, 3600)

        return active_subscriptions

    @property
    def active_plans(self):
        subscriptions = self.active_subscriptions
        plans = [subscription.plan for subscription in subscriptions]
        return plans

    @property
    def subscription_to_plan(self):
        subscriptions = self.active_subscriptions
        plans = {subscription.id: subscription.plan.id for subscription in subscriptions}
        return plans

    @property
    def plan_to_subscription(self):
        plans = {v: k for k, v in self.subscription_to_plan.items()}

        return plans

    def get_active_products(self):
        plans = self.active_plans
        products = [stripe.Product.retrieve(plan.product) for plan in plans]
        return products

    @property
    def plan_to_product(self):
        plans = self.active_plans
        products = {plan.id: stripe.Product.retrieve(plan.product).id for plan in plans}
        return products

    @property
    def prodct_to_plan(self):
        plans = self.active_plans
        products = {stripe.Product.retrieve(plan.product).id: plan.id for plan in plans}
        return products

    @property
    def active_products(self):
        products = cache.get(f"active_products_{self.id}")
        if products is not None:
            return products
        products = self.get_active_products()
        cache.set(f"active_products_{self.id}", products, 3600)

        return products

    @property
    def best_active_subscription(self):
        subscriptions = self.active_subscriptions
        return subscriptions[0]

    @property
    def best_active_product(self):
        products = self.active_products
        products.sort(key=lambda p: p.metadata.get('level', 0))
        return products[0]

    @property
    def best_active_subscription(self):
        subscription_id = self.plan_to_subscription[self.prodct_to_plan[self.best_active_product.id]]
        return stripe.Subscription.retrieve(subscription_id)

    @property
    def config(self):
        product = self.best_active_product
        return product.metadata

    def subscription_status(self):
        subscriptions = self.active_subscriptions()

    def cancel_subscription(self):
        for subscription in self.active_subscriptions:
            stripe.Subscription.modify(
                subscription.id,
                cancel_at_period_end=True,
            )
        self.refresh()

    def reactivate_subscription(self):
        for subscription in self.active_subscriptions:
            stripe.Subscription.modify(
                subscription.id,
                cancel_at_period_end=False,
            )
        self.refresh()
Search Use the search APIs to look up and retrieve objects in your Stripe data. Using search is a faster alternative to paginating through all resources. stripe.com Stripe has it’s own query language for querying data. I’m just getting into using it and it seems pretty good so far. I needed to lookup the price for products. I was able to find prices for my product using the python api as shown below.

Looking for a Heroku replacement, What I found was shocking!

Your browser does not support the audio element. I’ve long hosted my personal blog as a static site on waylonwalker.com. It’s all markdown, converted to html, and shipped as is. It’s been great, I’ve moved it from GitHub Pages, to Netlify, tried Vercel for a minute, and have landed on Cloudflare Pages. Each migration has not really been that hard, it’s just pointing ci to a different host after the site has built. What about server side Now the part that I have struggled with is how to cheaply host a server rendered application that can just live on forever without me paying for it. This is a harder problem as it costs more to keep servers spinning, memory, and disk all ready for you to use at a moments notice. Honestly I never really deployed anything that useful on heroku, but it seems like the klenex of the bunch that’s why they are in the title. I’ve moved between digital ocean and fly.io, and have had some great experiences with both. I just don’t want to build something that is g…