<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HoungDev</title>
    <description>The latest articles on DEV Community by HoungDev (@houngdev).</description>
    <link>https://dev.to/houngdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4067406%2F1d4834c6-90aa-4d87-b5c0-e860e9abb7ab.jpg</url>
      <title>DEV Community: HoungDev</title>
      <link>https://dev.to/houngdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/houngdev"/>
    <language>en</language>
    <item>
      <title>Beyond Login: Building a Production Authentication Lifecycle in FastAPI</title>
      <dc:creator>HoungDev</dc:creator>
      <pubDate>Sun, 09 Aug 2026 09:29:37 +0000</pubDate>
      <link>https://dev.to/houngdev/beyond-login-building-a-production-authentication-lifecycle-in-fastapi-1cc1</link>
      <guid>https://dev.to/houngdev/beyond-login-building-a-production-authentication-lifecycle-in-fastapi-1cc1</guid>
      <description>&lt;p&gt;Authentication is often presented as a short sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accept a username and password.&lt;/li&gt;
&lt;li&gt;Return a JWT.&lt;/li&gt;
&lt;li&gt;Protect a few endpoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is enough for a tutorial, but it is not an authentication lifecycle.&lt;/p&gt;

&lt;p&gt;Real applications must also answer harder questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How is an email address verified without storing a reusable secret?&lt;/li&gt;
&lt;li&gt;What happens to existing sessions after a password reset?&lt;/li&gt;
&lt;li&gt;Can a user see and revoke a lost device?&lt;/li&gt;
&lt;li&gt;How do we prevent a rotated refresh token from being replayed?&lt;/li&gt;
&lt;li&gt;How should TOTP secrets and recovery codes be stored?&lt;/li&gt;
&lt;li&gt;How can an OIDC identity be linked without trusting email matching?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I explored those questions while building&lt;br&gt;
&lt;a href="https://github.com/HoungDev/fastapi-production-api/releases/tag/v1.2.0" rel="noopener noreferrer"&gt;FastAPI Production API v1.2.0&lt;/a&gt;,&lt;br&gt;
a backward-compatible authentication lifecycle release for an open-source&lt;br&gt;
FastAPI backend foundation.&lt;/p&gt;

&lt;p&gt;This article explains the design decisions behind it—not just the endpoints&lt;br&gt;
that were added.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Model lifecycle tokens as scoped, single-use credentials
&lt;/h2&gt;

&lt;p&gt;Email verification and password recovery look similar from the outside: send a&lt;br&gt;
link, receive a token, and update an account. Treating them as interchangeable,&lt;br&gt;
however, creates unnecessary risk.&lt;/p&gt;

&lt;p&gt;The release uses account-action tokens with four important properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Random:&lt;/strong&gt; the token is generated as an opaque secret rather than derived
from user data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped:&lt;/strong&gt; a verification token cannot be used as a password-reset token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiring:&lt;/strong&gt; every token has a short, configurable lifetime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single use:&lt;/strong&gt; confirmation atomically marks the token as consumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only a hash of the token is persisted. The original value exists only long&lt;br&gt;
enough to be delivered to the user.&lt;/p&gt;

&lt;p&gt;This gives email verification and password reset a shared security primitive&lt;br&gt;
without making their policies identical.&lt;/p&gt;

&lt;p&gt;The main endpoints are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /auth/email-verification/request
POST /auth/email-verification/confirm
POST /auth/password-reset/request
POST /auth/password-reset/confirm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both request operations return uniform responses. A caller should not be able&lt;br&gt;
to determine whether an email belongs to an account by comparing status codes&lt;br&gt;
or response bodies.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Treat password reset as a session-security event
&lt;/h2&gt;

&lt;p&gt;Changing a password is not only a database update. If a stolen refresh token&lt;br&gt;
remains valid afterward, the attacker may keep creating new access tokens even&lt;br&gt;
though the account owner completed recovery.&lt;/p&gt;

&lt;p&gt;A successful reset therefore performs one transaction that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;validates and consumes the scoped reset token;&lt;/li&gt;
&lt;li&gt;replaces the password hash;&lt;/li&gt;
&lt;li&gt;consumes other outstanding reset tokens; and&lt;/li&gt;
&lt;li&gt;revokes every refresh-token session for the account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The endpoint does not issue a new authenticated session. The user signs in&lt;br&gt;
again with the new password.&lt;/p&gt;

&lt;p&gt;There is an important boundary here: existing stateless access tokens remain&lt;br&gt;
valid until their short expiration time. Immediate access-token invalidation&lt;br&gt;
would require a denylist, token-version check, or a move toward opaque tokens.&lt;br&gt;
Documenting that boundary is part of the security design.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Use refresh-token families to represent device sessions
&lt;/h2&gt;

&lt;p&gt;A table of individual refresh tokens does not naturally answer “Which devices&lt;br&gt;
are signed in?” Token rotation creates a chain of records, while the user thinks&lt;br&gt;
in terms of sessions.&lt;/p&gt;

&lt;p&gt;v1.2.0 groups rotated refresh tokens into a &lt;strong&gt;family&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login
  -&amp;gt; create family A for "Work Laptop"
  -&amp;gt; issue refresh token A1

refresh A1
  -&amp;gt; revoke A1
  -&amp;gt; issue A2 inside family A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This family becomes the device-session boundary. It supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET    /auth/sessions
DELETE /auth/sessions/{session_id}
DELETE /auth/sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Device labels are bounded and normalized before storage. Session responses&lt;br&gt;
expose useful metadata without returning token hashes or raw credentials.&lt;/p&gt;

&lt;p&gt;The family also improves replay handling. If a refresh token that was already&lt;br&gt;
consumed by rotation appears again, the live family is revoked. The system&lt;br&gt;
treats reuse as possible token theft rather than an ordinary validation error.&lt;/p&gt;

&lt;p&gt;Logout revokes the full family, not only one token record. Password reset, MFA&lt;br&gt;
changes, and external-identity changes can revoke all relevant families through&lt;br&gt;
the same session abstraction.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Add TOTP without turning the seed into a password
&lt;/h2&gt;

&lt;p&gt;TOTP verification requires access to the shared secret, so hashing the seed is&lt;br&gt;
not sufficient. The implementation encrypts TOTP seeds at rest using a&lt;br&gt;
dedicated application key.&lt;/p&gt;

&lt;p&gt;Recovery codes have different requirements. They only need comparison, so the&lt;br&gt;
database stores hashes and shows the original codes once when they are created.&lt;br&gt;
Each recovery code is single use.&lt;/p&gt;

&lt;p&gt;Enrollment is a two-step process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /auth/mfa/totp/enroll
POST /auth/mfa/totp/confirm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first operation creates a pending encrypted secret. MFA is not enabled&lt;br&gt;
until the user proves possession by submitting a valid code.&lt;/p&gt;

&lt;p&gt;After enrollment, password login no longer returns access and refresh tokens&lt;br&gt;
immediately. It returns a short-lived opaque challenge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;password accepted
  -&amp;gt; mfa_required
  -&amp;gt; challenge_token
  -&amp;gt; TOTP or unused recovery code
  -&amp;gt; local access and refresh tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service records the last accepted TOTP counter and rejects the same or an&lt;br&gt;
older counter. This prevents a valid code from being replayed inside the&lt;br&gt;
accepted clock window.&lt;/p&gt;

&lt;p&gt;Access tokens include authentication-method and authentication-time claims.&lt;br&gt;
Those claims provide a foundation for requiring recent MFA before sensitive&lt;br&gt;
operations. Refreshing a session intentionally does not manufacture a recent&lt;br&gt;
MFA event.&lt;/p&gt;

&lt;p&gt;TOTP is useful, but it is not phishing resistant. WebAuthn/passkeys remain the&lt;br&gt;
stronger direction for applications with that requirement.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Make OIDC linking an explicit security ceremony
&lt;/h2&gt;

&lt;p&gt;OIDC login uses the Authorization Code flow with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PKCE S256;&lt;/li&gt;
&lt;li&gt;random state;&lt;/li&gt;
&lt;li&gt;OIDC nonce;&lt;/li&gt;
&lt;li&gt;exact configured redirect URIs;&lt;/li&gt;
&lt;li&gt;browser-bound authorization transactions; and&lt;/li&gt;
&lt;li&gt;strict issuer, signature, audience, authorized-party, and nonce validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database stores hashes of state, nonce, and browser binding. The&lt;br&gt;
transaction-specific PKCE verifier must later be sent to the provider, so it is&lt;br&gt;
encrypted rather than hashed.&lt;/p&gt;

&lt;p&gt;External accounts are identified by the immutable pair &lt;code&gt;(issuer, subject)&lt;/code&gt;.&lt;br&gt;
Provider email is useful profile data, but it is not the identity key.&lt;/p&gt;

&lt;p&gt;Most importantly, an existing local account is never silently linked because&lt;br&gt;
an OIDC provider returned the same email address. Automatic email matching can&lt;br&gt;
turn a provider mistake or account-reassignment edge case into account takeover.&lt;/p&gt;

&lt;p&gt;Linking is therefore explicit and authenticated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /auth/oidc/link/authorize
GET  /auth/oidc/callback
GET  /auth/oidc/identities
DELETE /auth/oidc/identities/{identity_id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensitive link and unlink operations require recent authentication and revoke&lt;br&gt;
refresh sessions after the identity set changes. The application also prevents&lt;br&gt;
a user from removing their last available sign-in method.&lt;/p&gt;

&lt;p&gt;OIDC does not bypass local MFA. If a linked local account has MFA enabled, a&lt;br&gt;
successful provider response produces the same second-factor challenge used by&lt;br&gt;
password login.&lt;/p&gt;

&lt;h2&gt;
  
  
  What made this a release rather than a demo?
&lt;/h2&gt;

&lt;p&gt;The implementation was shipped through five focused slices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Email identity and verification&lt;/li&gt;
&lt;li&gt;Password recovery and session revocation&lt;/li&gt;
&lt;li&gt;Refresh-token families and device sessions&lt;/li&gt;
&lt;li&gt;TOTP MFA and recovery codes&lt;/li&gt;
&lt;li&gt;OIDC login and explicit account linking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every slice included tests, documentation, failure paths, and migration checks.&lt;br&gt;
The final v1.2.0 release passed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;144 automated tests;&lt;/li&gt;
&lt;li&gt;92.85% coverage against a 90% gate;&lt;/li&gt;
&lt;li&gt;linting and formatting checks;&lt;/li&gt;
&lt;li&gt;Alembic migration and rollback validation;&lt;/li&gt;
&lt;li&gt;dependency auditing;&lt;/li&gt;
&lt;li&gt;source distribution and wheel builds; and&lt;/li&gt;
&lt;li&gt;an isolated wheel installation and application-version smoke test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release also publishes SHA-256 checksums alongside the Python artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it or review the design
&lt;/h2&gt;

&lt;p&gt;Repository:&lt;br&gt;
&lt;a href="https://github.com/HoungDev/fastapi-production-api" rel="noopener noreferrer"&gt;github.com/HoungDev/fastapi-production-api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Release:&lt;br&gt;
&lt;a href="https://github.com/HoungDev/fastapi-production-api/releases/tag/v1.2.0" rel="noopener noreferrer"&gt;FastAPI Production API v1.2.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is intended as a security-focused reference foundation, not a claim&lt;br&gt;
that one authentication policy fits every production system. Provider setup,&lt;br&gt;
email delivery, secret management, token lifetimes, access-token invalidation,&lt;br&gt;
and threat models still need application-specific decisions.&lt;/p&gt;

&lt;p&gt;I would especially value feedback on the refresh-family model, MFA step-up&lt;br&gt;
claims, and OIDC linking rules.&lt;/p&gt;

&lt;p&gt;I also shared the release summary on&lt;br&gt;
&lt;a href="https://www.linkedin.com/posts/houng-dev-232907427_fastapi-python-opensource-share-7492145563480580096-uxUD/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What authentication lifecycle failure mode has caused the most trouble in your&lt;br&gt;
own systems?&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Designing a Safer Batch Image Renamer in Python</title>
      <dc:creator>HoungDev</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:23:34 +0000</pubDate>
      <link>https://dev.to/houngdev/designing-a-safer-batch-image-renamer-in-python-3gjm</link>
      <guid>https://dev.to/houngdev/designing-a-safer-batch-image-renamer-in-python-3gjm</guid>
      <description>&lt;p&gt;Batch-renaming images sounds like a five-line script—until a destination already exists, a rename fails halfway through, or you need to recover the original filenames.&lt;/p&gt;

&lt;p&gt;I ran into that design problem while building &lt;a href="https://github.com/HoungDev/creator-toolkit-cli" rel="noopener noreferrer"&gt;Creator Toolkit CLI&lt;/a&gt;, an open-source Python CLI for small creator workflows. The interesting part was not generating &lt;code&gt;image_1.jpg&lt;/code&gt;; it was making the operation inspectable before it ran and recoverable afterward.&lt;/p&gt;

&lt;p&gt;This article walks through the safety model shipped in Creator Toolkit CLI v0.1.0: deterministic planning, dry runs, collision checks, two-phase renaming, rollback, and undo manifests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F372jpqb7b8te66j470hf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F372jpqb7b8te66j470hf.gif" alt="A short terminal demo of Creator Toolkit CLI v0.1.0 generating a title and tags, then previewing two image renames" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious loop is risky
&lt;/h2&gt;

&lt;p&gt;A first implementation might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is short, but it leaves several questions unanswered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the input order stable across systems?&lt;/li&gt;
&lt;li&gt;What happens if &lt;code&gt;image_1.jpg&lt;/code&gt; already exists?&lt;/li&gt;
&lt;li&gt;What if that file is also one of the sources that should move?&lt;/li&gt;
&lt;li&gt;What happens when the third rename fails after two succeeded?&lt;/li&gt;
&lt;li&gt;Can a user preview the exact plan?&lt;/li&gt;
&lt;li&gt;Can the operation be reversed later?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For file mutations, those are not edge cases. They are the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate planning from mutation
&lt;/h2&gt;

&lt;p&gt;The first rule is simple: build the complete plan without changing the directory.&lt;/p&gt;

&lt;p&gt;Creator Toolkit selects supported files, sorts them case-insensitively, and returns explicit &lt;code&gt;(source, destination)&lt;/code&gt; operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SUPPORTED_EXTENSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plan_image_renames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_EXTENSIONS&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;casefold&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;operations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;operations&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Planning separately gives the CLI one source of truth for preview, confirmation, JSON output, application, and tests.&lt;/p&gt;

&lt;p&gt;Users can inspect that plan without touching a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creator-toolkit rename ./images &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planned 2 image(s).
cover.jpg -&amp;gt; image_1.jpg
thumbnail.png -&amp;gt; image_2.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dry run is much more useful when it exercises the same planning path as the real operation. A separate “preview approximation” can drift from the code that eventually mutates the filesystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Validate the whole plan before the first rename
&lt;/h2&gt;

&lt;p&gt;Before applying anything, the implementation checks that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every source is unique and still exists;&lt;/li&gt;
&lt;li&gt;every destination is unique;&lt;/li&gt;
&lt;li&gt;each operation stays inside the same directory; and&lt;/li&gt;
&lt;li&gt;an existing destination is either another source in the plan or a collision that must stop the operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last distinction matters. Consider this directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cover.jpg
image_1.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The desired plan may be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cover.jpg   -&amp;gt; image_1.jpg
image_1.jpg -&amp;gt; image_2.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;image_1.jpg&lt;/code&gt; exists, but it is also scheduled to move. Rejecting every existing destination would incorrectly reject a valid plan. Overwriting it would be worse.&lt;/p&gt;

&lt;p&gt;Complete-plan validation lets us distinguish a movable destination from an unrelated file that must never be overwritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use a two-phase rename
&lt;/h2&gt;

&lt;p&gt;Even a valid plan can fail if it is applied directly in source order. Creator Toolkit therefore uses two phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move every source to a unique temporary name in the same directory.&lt;/li&gt;
&lt;li&gt;Move each temporary file to its final destination.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cover.jpg   -&amp;gt; .creator-toolkit-stage-&amp;lt;uuid&amp;gt;.tmp
image_1.jpg -&amp;gt; .creator-toolkit-stage-&amp;lt;uuid&amp;gt;.tmp

.creator-toolkit-stage-&amp;lt;uuid&amp;gt;.tmp -&amp;gt; image_1.jpg
.creator-toolkit-stage-&amp;lt;uuid&amp;gt;.tmp -&amp;gt; image_2.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once phase one completes, all final destination names are free. This avoids order-dependent collisions without relying on platform-specific overwrite behavior.&lt;/p&gt;

&lt;p&gt;The temporary files remain in the target directory so each move stays on the same filesystem. That avoids cross-device rename failures and keeps the operation local to the directory being changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Roll back both failure points
&lt;/h2&gt;

&lt;p&gt;There are two different failure windows, so they need two recovery paths.&lt;/p&gt;

&lt;p&gt;If staging fails, files that already reached temporary names are restored to their original names in reverse order.&lt;/p&gt;

&lt;p&gt;If finalization fails, the state is mixed: some files have final names, while others still have temporary names. The implementation stages that mixed set again and then restores every original source name.&lt;/p&gt;

&lt;p&gt;The important principle is broader than this project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A multi-step filesystem mutation should define recovery for every intermediate state it can create.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rollback is still best effort. Filesystems and external processes can fail in ways an application cannot fully repair. The CLI reports a distinct runtime error if restoration also fails, and the documentation still recommends a separate backup for valuable assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Write recovery metadata before applying
&lt;/h2&gt;

&lt;p&gt;Interactive confirmation protects the current command, but it does not help tomorrow. Applied CLI renames therefore create a unique JSON manifest in the image directory.&lt;/p&gt;

&lt;p&gt;A simplified manifest looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"applied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-07T12:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/images"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cover.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image_1.jpg"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thumbnail.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image_2.png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applied_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-07T12:00:01+00:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest is first written with &lt;code&gt;status: "pending"&lt;/code&gt;, then changed to &lt;code&gt;applied&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. Manifest writes themselves use a temporary file followed by a replace operation, so an interrupted write is less likely to leave truncated JSON at the final path.&lt;/p&gt;

&lt;p&gt;To preview a restore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creator-toolkit undo ./images/.creator-toolkit-renames-&amp;lt;timestamp&amp;gt;-&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.json &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creator-toolkit undo ./images/.creator-toolkit-renames-&amp;lt;timestamp&amp;gt;-&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Undo reverses the concrete operations stored in the manifest. It does not try to reconstruct intent from the current directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Treat the manifest as untrusted input
&lt;/h2&gt;

&lt;p&gt;A recovery file can become dangerous if the program blindly trusts paths inside it.&lt;/p&gt;

&lt;p&gt;Before undoing, Creator Toolkit checks the manifest version and status, requires an absolute target directory, and accepts only plain filenames for each operation. Values such as &lt;code&gt;../outside.txt&lt;/code&gt; are rejected because &lt;code&gt;Path(value).name != value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reversed plan then passes through the same collision validation as a normal rename. If a new file now occupies an original filename, undo stops instead of overwriting it.&lt;/p&gt;

&lt;p&gt;That makes the manifest useful recovery metadata, not a general-purpose instruction file for moving arbitrary paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make automation explicit
&lt;/h2&gt;

&lt;p&gt;Human-readable previews are useful at a terminal. Scripts need a stable contract.&lt;/p&gt;

&lt;p&gt;Every scriptable subcommand supports JSON output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creator-toolkit rename ./images &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Successful responses use a versioned envelope, while validation and runtime errors go to standard error as structured JSON. Exit codes distinguish success, runtime failure, and unsafe CLI usage.&lt;/p&gt;

&lt;p&gt;JSON mode also never prompts. A mutating rename or undo must include either &lt;code&gt;--dry-run&lt;/code&gt; or &lt;code&gt;--yes&lt;/code&gt;; otherwise the command exits with a usage error. Automation should never hang while waiting for an interactive answer it cannot provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the released version
&lt;/h2&gt;

&lt;p&gt;Creator Toolkit CLI v0.1.0 requires Python 3.11 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;creator-toolkit-cli&lt;span class="o"&gt;==&lt;/span&gt;0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with a disposable directory and a preview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creator-toolkit rename ./images &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then review the plan before applying it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creator-toolkit rename ./images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source, tests, JSON contract, and release artifacts are available here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/HoungDev/creator-toolkit-cli" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/creator-toolkit-cli/0.1.0/" rel="noopener noreferrer"&gt;Package on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HoungDev/creator-toolkit-cli/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;v0.1.0 release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HoungDev/creator-toolkit-cli/blob/v0.1.0/docs/json-output.md" rel="noopener noreferrer"&gt;JSON output reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HoungDev/creator-toolkit-cli/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article intentionally targets the released &lt;code&gt;v0.1.0&lt;/code&gt; tag. The &lt;code&gt;main&lt;/code&gt; branch may contain unreleased changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The core algorithm is not large. The reliability comes from the boundaries around it: plan first, validate globally, isolate intermediate states, define rollback, persist recovery metadata, and make automation behavior explicit.&lt;/p&gt;

&lt;p&gt;If you have handled batch file mutations differently, I would be interested in the failure cases and recovery strategies that shaped your design.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
