<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Python on Blog</title><link>https://blog.hoshiyomi-dev.work/tags/python/</link><description>Recent content in Python on Blog</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 11 Oct 2026 10:54:05 +0000</lastBuildDate><atom:link href="https://blog.hoshiyomi-dev.work/tags/python/index.xml" rel="self" type="application/rss+xml"/><item><title>DragonBot Ticket Close Hang</title><link>https://blog.hoshiyomi-dev.work/post/dragonbot-ticket-close-hang/</link><pubDate>Sat, 10 Oct 2026 18:25:00 +0800</pubDate><guid>https://blog.hoshiyomi-dev.work/post/dragonbot-ticket-close-hang/</guid><description>&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/xDDoubleTea/DragonBot"&gt;DragonBot&lt;/a&gt; is a customer-support ticket bot for a purchasing-agent business on Discord, written with &lt;code&gt;discord.py&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A customer opens a ticket, which is a private channel visible only to the customer and staff.&lt;/li&gt;
&lt;li&gt;Every status change renames the channel to &lt;code&gt;{type}-{id:04d}-{status}&lt;/code&gt;, so staff can see the status from the channel list.&lt;/li&gt;
&lt;li&gt;Closing a ticket sets the status to &lt;code&gt;CLOSED&lt;/code&gt;, exports the conversation with DiscordChatExporter, posts the transcript to an archive channel, and DMs the customer a feedback form.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-happened"&gt;What Happened&lt;/h2&gt;
&lt;p&gt;After a staff member confirmed closing a ticket, the bot sometimes sat on 「正在生成頻道紀錄以及傳送回饋單給顧客&amp;hellip;」 for up to 10 minutes before the after-close buttons appeared. The owner reported it too.&lt;/p&gt;</description></item><item><title>LeetCodeBot: One Session, Many Commands</title><link>https://blog.hoshiyomi-dev.work/post/leetcodebot-shared-session/</link><pubDate>Fri, 28 Aug 2026 20:27:00 +0800</pubDate><guid>https://blog.hoshiyomi-dev.work/post/leetcodebot-shared-session/</guid><description>&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/xDDoubleTea/LeetCodeBot"&gt;LeetCodeBot&lt;/a&gt; talks to its database through &lt;code&gt;AsyncDatabaseManager&lt;/code&gt;, an async context manager that hands out SQLAlchemy sessions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_database_manager&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# leaving the block commits, or rolls back on error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;main.py&lt;/code&gt; creates &lt;strong&gt;one&lt;/strong&gt; manager and shares it across every cog. The old version looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__aenter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sessionmaker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_tb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It has two bugs (&lt;a href="https://github.com/xDDoubleTea/LeetCodeBot/issues/38"&gt;#38&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id="bug-1-__aexit__-commits-the-wrong-session"&gt;Bug 1: &lt;code&gt;__aexit__&lt;/code&gt; commits the wrong session&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;__aenter__&lt;/code&gt; returns a new session, so the caller&amp;rsquo;s &lt;code&gt;db&lt;/code&gt; is bound to that object. But &lt;code&gt;__aexit__&lt;/code&gt; reads &lt;code&gt;self.session&lt;/code&gt;, which belongs to the shared manager.&lt;/p&gt;</description></item><item><title>LeetCodeBot: The Bug the Async Migration Did Not Write</title><link>https://blog.hoshiyomi-dev.work/post/leetcodebot-missing-greenlet/</link><pubDate>Fri, 28 Aug 2026 20:27:00 +0800</pubDate><guid>https://blog.hoshiyomi-dev.work/post/leetcodebot-missing-greenlet/</guid><description>&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/xDDoubleTea/LeetCodeBot"&gt;LeetCodeBot&lt;/a&gt; is a Discord bot for looking up and discussing LeetCode problems. It stores problems and their topic tags with SQLAlchemy on SQLite.&lt;/p&gt;
&lt;p&gt;In August 2026, &lt;a href="https://github.com/xDDoubleTea/LeetCodeBot/pull/35"&gt;#35&lt;/a&gt; moved the database layer from synchronous SQLAlchemy to async (&lt;code&gt;sqlite+aiosqlite&lt;/code&gt;). The bot kept running in production afterwards, with no errors.&lt;/p&gt;
&lt;h2 id="what-happened"&gt;What happened&lt;/h2&gt;
&lt;p&gt;A fresh clone could not start the bot. Deleting the database file to &amp;ldquo;start clean&amp;rdquo; made it fail every time:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can&amp;#39;t call await_only() here. Was IO attempted in an unexpected place?
File &amp;#34;core/leetcode_problem.py&amp;#34;, line 420, in add_problem_to_db
if db_tag not in db_problem.tags:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The failing expression is a plain attribute read: &lt;code&gt;db_problem.tags&lt;/code&gt;.&lt;/p&gt;</description></item></channel></rss>