<?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>Raven's eyrie</title><link>https://ravencentric.cc/</link><description>Recent content on Raven's eyrie</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><managingEditor>me@ravencentric.cc (Ravencentric)</managingEditor><webMaster>me@ravencentric.cc (Ravencentric)</webMaster><copyright>Ravencentric (CC-BY-4.0)</copyright><atom:link href="https://ravencentric.cc/index.xml" rel="self" type="application/rss+xml"/><item><title>Projects</title><link>https://ravencentric.cc/projects/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>me@ravencentric.cc (Ravencentric)</author><guid>https://ravencentric.cc/projects/</guid><description>&lt;p&gt;A chronologically sorted list of my projects. This doesn’t cover &lt;em&gt;every&lt;/em&gt; project I’ve
worked on, but it does cover every project I wanted to talk about.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#2022"&gt;2022&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#awesome-arr"&gt;awesome-arr&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#2023"&gt;2023&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#juicenet-cli"&gt;juicenet-cli&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#2024"&gt;2024&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#pyanilist"&gt;pyanilist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#pynyaa"&gt;pynyaa&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#archivefile"&gt;archivefile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#nzb"&gt;nzb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#seadex"&gt;seadex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#stringenum"&gt;stringenum&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#2025"&gt;2025&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#nzb-rs"&gt;nzb-rs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rnzb"&gt;rnzb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#atomicwriter"&gt;atomicwriter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#privatebin"&gt;PrivateBin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#myne"&gt;myne&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#mkvinfo"&gt;mkvinfo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#misaki"&gt;misaki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#2026"&gt;2026&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#ravencentricgithubio"&gt;ravencentric.github.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="2022"&gt;2022&lt;/h2&gt;
&lt;h3 id="awesome-arr"&gt;awesome-arr&lt;/h3&gt;
&lt;p&gt;*A collection of &lt;em&gt;arrs and related stuff&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/Ravencentric/awesome-arr"&gt;[GitHub]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Essentially my first project on GitHub, which didn&amp;rsquo;t involve any code whatsoever.&lt;/p&gt;
&lt;p&gt;By the time we get here, I was already deep into self-hosting: Sonarr, Radarr, Plex,
etc. During this time, I came across a cool repository, &lt;a href="http://web.archive.org/web/20220913033723/https://github.com/rustyshackleford36/locatarr"&gt;rustyshackleford36/locatarr&lt;/a&gt;,
that collected *arr-family apps. I used to check it every now and then, but at some
point the repository disappeared, so I decided to make my own while also greatly
expanding on the original collection. To my surprise, it somehow ended up with over 3k
stars on GitHub.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A chronologically sorted list of my projects. This doesn’t cover <em>every</em> project I’ve
worked on, but it does cover every project I wanted to talk about.</p>
<ul>
<li><a href="#2022">2022</a>
<ul>
<li><a href="#awesome-arr">awesome-arr</a></li>
</ul>
</li>
<li><a href="#2023">2023</a>
<ul>
<li><a href="#juicenet-cli">juicenet-cli</a></li>
</ul>
</li>
<li><a href="#2024">2024</a>
<ul>
<li><a href="#pyanilist">pyanilist</a></li>
<li><a href="#pynyaa">pynyaa</a></li>
<li><a href="#archivefile">archivefile</a></li>
<li><a href="#nzb">nzb</a></li>
<li><a href="#seadex">seadex</a></li>
<li><a href="#stringenum">stringenum</a></li>
</ul>
</li>
<li><a href="#2025">2025</a>
<ul>
<li><a href="#nzb-rs">nzb-rs</a></li>
<li><a href="#rnzb">rnzb</a></li>
<li><a href="#atomicwriter">atomicwriter</a></li>
<li><a href="#privatebin">PrivateBin</a></li>
<li><a href="#myne">myne</a></li>
<li><a href="#mkvinfo">mkvinfo</a></li>
<li><a href="#misaki">misaki</a></li>
</ul>
</li>
<li><a href="#2026">2026</a>
<ul>
<li><a href="#ravencentricgithubio">ravencentric.github.io</a></li>
</ul>
</li>
</ul>
<hr>
<h2 id="2022">2022</h2>
<h3 id="awesome-arr">awesome-arr</h3>
<p>*A collection of <em>arrs and related stuff</em></p>
<p><a href="https://github.com/Ravencentric/awesome-arr">[GitHub]</a></p>
<p>Essentially my first project on GitHub, which didn&rsquo;t involve any code whatsoever.</p>
<p>By the time we get here, I was already deep into self-hosting: Sonarr, Radarr, Plex,
etc. During this time, I came across a cool repository, <a href="http://web.archive.org/web/20220913033723/https://github.com/rustyshackleford36/locatarr">rustyshackleford36/locatarr</a>,
that collected *arr-family apps. I used to check it every now and then, but at some
point the repository disappeared, so I decided to make my own while also greatly
expanding on the original collection. To my surprise, it somehow ended up with over 3k
stars on GitHub.</p>
<h2 id="2023">2023</h2>
<h3 id="juicenet-cli">juicenet-cli</h3>
<p><em>CLI tool designed to simplify the process of uploading files to Usenet</em></p>
<p><a href="https://github.com/Ravencentric/juicenet-cli">[GitHub]</a>
<a href="https://pypi.org/project/juicenet-cli/">[PyPI]</a>
<a href="https://juicenet.ravencentric.cc/">[Docs]</a></p>
<p>For all intents and purposes, this was the first piece of code I ever wrote that was
more complex than Fibonacci. It started off as a <a href="https://github.com/Ravencentric/juicenet-cli/tree/ceff3b9e97a173795d2a2b1a8a38052997b20e00">single-file script</a> that you couldn&rsquo;t
even install from PyPI because I had no idea how to package anything.</p>
<p>If you don&rsquo;t know much about uploading to usenet: you can&rsquo;t upload folders, only
individual files, and you should obfuscate them. Typical workflow is that your uploader
splits the file into pieces called &ldquo;articles&rdquo; (if you&rsquo;re familiar with torrenting, this
is similar), uploads them, and records them in an <a href="https://en.wikipedia.org/wiki/NZB">NZB</a> file. Downloaders then use the
NZB to grab each article and reconstruct the file. If a single article is lost, deleted,
or corrupted, everything breaks, so there are also parity files called <a href="https://en.wikipedia.org/wiki/Parchive#Par2">PAR2</a> which are
generated for a given data file and uploaded together, allowing the client to repair the
file up to a certain threshold.</p>
<p>Now, pretty much all existing Usenet uploaders wrap your file or folder in split,
obfuscated RAR archives which then get split into articles. They then write this
obfuscated nonsense in the NZB file, which means you can no longer statically parse it
to get any metadata. And obfuscating the NZB is worse than useless. You only really
wanna obfuscate the data, because once you have the NZB you can
download the file regardless. The RAR step may have served some purpose 20 years ago but
it&rsquo;s entirely wasteful now. It&rsquo;s probably a mix of history, people following existing
practices blindly, and the misconception that RARing is necessary for obfuscation and/or
preserving a folder.</p>
<p>And you know what else PAR2 files can do? They can rename the file to its original name,
which you can use to reconstruct the folder structure. So we no longer need RARs for
that. RAR is also unnecessary for obfuscation, since that already happens at the article
level. While testing this setup, I also found a <a href="https://github.com/sabnzbd/sabnzbd/issues/2626">bug</a> in SABnzbd where it failed to
correctly reconstruct the folder structure from PAR2 alone, which got fixed pretty
quickly.</p>
<p>Now, I&rsquo;m not the first one to notice any of this. Everything I&rsquo;ve said here I learned
from <a href="https://github.com/animetosho">@animetosho</a>&rsquo;s extremely well written article, <a href="https://github.com/animetosho/Nyuu/wiki/Stop-RAR-Uploads">Stop RAR Uploads</a>, which goes into
more detail. They also happen to maintain the best Usenet tooling there is: <a href="https://github.com/animetosho/Nyuu">Nyuu</a> as
the uploader, and <a href="https://github.com/animetosho/ParPar">ParPar</a> as the PAR2 generator. Nyuu
<a href="https://github.com/animetosho/Nyuu/issues/58">doesn&rsquo;t generate PAR2 files by default</a> and ParPar doesn&rsquo;t preserve anything but
the basename by default, which makes sense, as they can&rsquo;t really assume what the user
wants.</p>
<p>But I can.</p>
<p>So the next step was familiarizing myself with Nyuu and ParPar. Mostly ParPar, since I
had to figure out how to preserve folders with ParPar&rsquo;s <a href="https://juicenet.ravencentric.cc/archive/parpar-filepath-formats/"><code>--filepath-format</code></a> option.
After that, I wrote a tiny script that calls ParPar and Nyuu with the correct arguments
and called it Juicenet.</p>
<p>The quality of the code in Juicenet hasn&rsquo;t aged well. It&rsquo;s very much a novice&rsquo;s first
attempt and it shows.</p>
<p>Still, it has gained more users than I ever expected, likely because to this day it&rsquo;s
one of the few, if not the only, high level uploaders that does everything without using
RAR archives. I can definitely do way better if I rewrote it from scratch, because
that&rsquo;s probably the only way I&rsquo;d get rid of every architectural mistake I made, but the
fact that this has real users means I&rsquo;ll end up breaking them, so it&rsquo;s not exactly an
easy choice.</p>
<h2 id="2024">2024</h2>
<h3 id="pyanilist">pyanilist</h3>
<p><em>Python wrapper for the AniList API</em></p>
<p><a href="https://github.com/Ravencentric/pyanilist">[GitHub]</a>
<a href="https://pypi.org/project/pyanilist/">[PyPI]</a>
<a href="https://ravencentric.cc/pyanilist/">[Docs]</a></p>
<p>I use the <a href="https://docs.anilist.co/">AniList API</a> in a lot of scripts to manage my self-hosted collection. For a
while I stuck with existing libraries, but issues kept cropping up and I was never
really happy with them - most lacked proper type hints, structured objects, or both.
Some also seemed entirely unmaintained. After enough <code>TypeError</code>s and expressions like
<code>data[&quot;Media&quot;][0][&quot;title&quot;][&quot;english&quot;]</code>, I finally gave up and started writing my own.</p>
<p>How hard can an API wrapper be anyway? Just parse the API and be done with it. Except
AniList is GraphQL, with a pretty large surface area, circular relationships, and some
awkward response shapes. That probably explains why nobody seemed eager to maintain an
AniList library. After thinking about it, I decided I would rather have an ergonomic API
than try to cover everything AniList exposes, so I narrowed things down to what I
actually needed and focused on making that simple.</p>
<p>I also ended up post-processing responses because AniList is not particularly
consistent. Sometimes &ldquo;empty&rdquo; nested objects have every field set to null, sometimes the
entire object is just null, and arrays occasionally contain null elements. The wrapper
normalizes these cases so the returned values match the type hints and require fewer
None checks.</p>
<p>The first version of this library released with 9 required dependencies. This wasn&rsquo;t a
problem for me at the time, but as I worked on more projects I started developing a
stronger stance on unnecessary dependencies. For example, a dependency that&rsquo;s slow to
update blocks my entire project from updating to the next Python version. So as I worked
on this further, I slowly removed most of them, to the point where the latest version
only depends on two things: a networking stack (httpx) and a data validation library
(msgspec).</p>
<p>On a slight tangent, this is also a project where I really wish Python had some form of
<a href="https://peps.python.org/pep-0505/"><code>None</code>-aware operator</a>.</p>
<h3 id="pynyaa">pynyaa</h3>
<p><em>Turn nyaa.si torrent pages into neat Python objects</em></p>
<p><a href="https://github.com/Ravencentric/pynyaa">[GitHub]</a>
<a href="https://pypi.org/project/pynyaa/">[PyPI]</a>
<a href="https://ravencentric.cc/pynyaa/">[Docs]</a></p>
<p>AniList metadata wasn&rsquo;t the only thing my scripts needed, but Nyaa does not offer any
API. I looked around for existing libraries but didn&rsquo;t find anything satisfactory.</p>
<p>So I wrote a small function that parsed the page for the few fields I needed. That
worked for a while, but it kept growing as I needed more metadata from Nyaa. At some
point it started hitting quirks of how the site represents things (a release can be both
trusted and a remake, but since the panel only has a single color, it just ends up red),
and eventually it got messy enough that I decided to turn it into a standalone library.</p>
<p>The first release tried to do too much and ended up with about ten dependencies. It
handled caching, parsed torrent files, used pydantic despite already validating
everything by hand, and pulled in lxml when the standard library would have been enough.</p>
<p>That made it harder to use in different contexts, since it forced those decisions onto
the user, so I started stripping those pieces out. These days it&rsquo;s a lightweight library
that just focuses on parsing Nyaa pages, and only depends on two packages: a network
stack (httpx) and an HTML parser (beautifulsoup4).</p>
<p>At this point it covers pretty much every field Nyaa exposes and has completely replaced
my original scraping code. It returns type-safe, structured objects and works in both
sync and async code. Since I rely on it heavily in my own scripts, it has also ended up
fairly battle-tested against real-world cases.</p>
<h3 id="archivefile">archivefile</h3>
<p><em>Unified interface for tar, zip, sevenzip, and rar files</em></p>
<p><a href="https://github.com/Ravencentric/archivefile">[GitHub]</a>
<a href="https://pypi.org/project/archivefile/">[PyPI]</a>
<a href="https://ravencentric.cc/archivefile/">[Docs]</a></p>
<p>I was dealing with archive files of various formats and scripting against them quickly
became an annoying dance of if-else branches to handle the fact that <a href="https://docs.python.org/3/library/tarfile.html">tarfile</a>,
<a href="https://docs.python.org/3/library/zipfile.html">zipfile</a>, <a href="https://pypi.org/project/py7zr/">py7zr</a>, and <a href="https://pypi.org/project/rarfile/">rarfile</a> all behave differently
despite doing the same basic things. So I wrote <code>archivefile</code>.</p>
<p>On a high level, the <code>ArchiveFile</code> class is defined by a protocol with an API that
covers the common functionality. It&rsquo;s somewhat inspired by <a href="https://docs.python.org/3/library/pathlib.html">pathlib</a>, which I
think is great. I then implement this for the aforementioned formats. At runtime, it
does a single check to dispatch the correct handler, and I no longer have to write
boilerplate just to read a single file from an archive.</p>
<p>In fact, due to this approach, every method under <code>ArchiveFile</code> has a single line worth
of body:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-py" data-lang="py"><span class="line"><span class="ln">1</span><span class="cl">    <span class="k">def</span> <span class="nf">get_member</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">member</span><span class="p">:</span> <span class="n">StrPath</span> <span class="o">|</span> <span class="n">ArchiveMember</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ArchiveMember</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s2">        &lt;Docstring redacted for brevity&gt;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_adapter</span><span class="o">.</span><span class="n">get_member</span><span class="p">(</span><span class="n">member</span><span class="p">)</span></span></span></code></pre></div><p>It also has a fair amount of tests to ensure the handlers produce the same output
regardless of the underlying format.</p>
<p>Developing this led me to find various bugs and missing functionality in <a href="https://pypi.org/project/py7zr/">py7zr</a>,
which I fixed <a href="https://github.com/miurahr/py7zr">upstream</a>, becoming the
<a href="https://github.com/miurahr/py7zr/graphs/contributors">second highest committer on the project</a> with
<a href="https://github.com/miurahr/py7zr/pulls?q=is%3Apr+author%3ARavencentric+is%3Aclosed">15 or so merged PRs</a>. Thanks to the py7zr maintainer, <a href="https://github.com/miurahr">@miurahr</a>, for being
great to work with and accepting my PRs.</p>
<h3 id="nzb">nzb</h3>
<p><em>A spec compliant parser and meta editor for NZB files</em></p>
<p><a href="https://github.com/Ravencentric/NZB">[GitHub]</a>
<a href="https://pypi.org/project/NZB/">[PyPI]</a>
<a href="https://ravencentric.cc/NZB/">[Docs]</a></p>
<p>Possibly one of my favorites. It&rsquo;s a performant, pure Python, dependency free, type-safe
<a href="https://sabnzbd.org/wiki/extra/nzb-spec">spec</a> compliant parser and meta editor for NZB files following the &ldquo;make invalid states
unrepresentable&rdquo; pattern (well, as long as you stick to the public API, because it&rsquo;s
Python after all), so if you successfully construct it, you know it&rsquo;s a valid NZB
structure.</p>
<p>Before working on this, I was convinced XML is a scary format and even added a
dependency, <a href="https://pypi.org/project/xmltodict/"><code>xmltodict</code></a>, to avoid dealing with it, but as you can imagine the
resulting dict isn&rsquo;t pleasant to work with. XML just doesn&rsquo;t lend itself well to that
kind of transformation. After working on the Rust implementation (spoilers), which
forced me to deal with XML because there wasn&rsquo;t an equivalent to <code>xmltodict</code>, I realized
XML isn&rsquo;t scary at all. So I dropped <code>xmltodict</code> here as well and switched to the
stdlib&rsquo;s <a href="https://docs.python.org/3/library/xml.etree.elementtree.html"><code>xml.etree.ElementTree</code></a>, which is significantly faster and easier to work
with.</p>
<p>Beyond that, it also provides various ergonomic methods for introspecting itself. I&rsquo;ve
used it extensively on real world files, so I think I can confidently claim it&rsquo;s the
best NZB parser in Python, however niche that might be.</p>
<h3 id="seadex">seadex</h3>
<p><em>Python wrapper for the SeaDex API</em></p>
<p><a href="https://github.com/Ravencentric/seadex">[GitHub]</a>
<a href="https://pypi.org/project/seadex/">[PyPI]</a>
<a href="https://ravencentric.cc/seadex/">[Docs]</a></p>
<p>Pretty much what it says on the tin. It&rsquo;s a fairly standard API wrapper for <a href="https://releases.moe/about/">SeaDex</a>.
There&rsquo;s not much interesting to talk about here. By this point it&rsquo;s my third API
wrapper, so I had a decent idea of how to approach it. Development ended up being fairly
uneventful, in a good way.</p>
<h3 id="stringenum">stringenum</h3>
<p><em>A small, dependency-free library offering additional enum.StrEnum subclasses and a
backport for older Python versions</em></p>
<p><a href="https://github.com/Ravencentric/stringenum">[GitHub]</a>
<a href="https://pypi.org/project/stringenum/">[PyPI]</a></p>
<p>This was mostly a toy project because I wanted to play with <a href="https://docs.python.org/3/reference/datamodel.html#metaclasses">metaclasses</a>. It backports
features from newer Python versions down to 3.9 and provides a bunch of other string
enums with different properties and guarantees.</p>
<p>The somewhat interesting part here is that the invariants are enforced at construction
time, so you can always rely on them, and it&rsquo;s fully typed. Typing metaclasses was
pretty confusing to wrap my head around, but in the end I got it and this library plays
well with typecheckers.</p>
<p>I wouldn&rsquo;t recommend depending on this library. You should just copy paste the relevant
parts if you really need them.</p>
<h2 id="2025">2025</h2>
<h3 id="nzb-rs">nzb-rs</h3>
<p><em>A spec compliant parser for NZB files</em></p>
<p><a href="https://github.com/Ravencentric/NZB-rs">[GitHub]</a>
<a href="https://crates.io/crates/NZB-rs">[crates.io]</a>
<a href="https://docs.rs/NZB-rs/latest/NZB_rs/">[Docs]</a></p>
<p>Rust had been on my radar for a while, so one day I finally sat down and read the
<a href="https://doc.rust-lang.org/book/">Rust book</a>. Great tooling (cargo), null safety, a powerful type system, pattern
matching - there was a lot going for it. Naturally, I needed a project, and parsers
seemed like exactly the kind of thing Rust excels at. My <a href="https://github.com/Ravencentric/nzb">NZB parser</a> was a perfect
candidate: it deals with XML, a format with a long history of security pitfalls that
safe Rust largely avoids by design.</p>
<p>Moving from Python to Rust felt pretty natural, likely thanks to Rust&rsquo;s zero-cost
abstractions that are just as expressive as many Python constructs. Static typing wasn&rsquo;t
new to me either since I was already using type hints in Python.</p>
<p>Traits feel like a more powerful mix of dunder methods, <a href="https://docs.python.org/3/library/abc.html"><code>abc.ABC</code></a>, and
<a href="https://docs.python.org/3/library/typing.html#typing.Protocol"><code>typing.Protocol</code></a>. I miss Rust&rsquo;s enums every time I write Python, and <a href="https://doc.rust-lang.org/std/option/"><code>Option&lt;T&gt;</code></a>
would make any <a href="https://peps.python.org/pep-0505/">PEP-505</a> fans jealous (myself included).</p>
<p>The borrow checker rarely got in my way. The rules are straightforward: a value has a
single owner, and only one mutable reference at a time. I also barely had to think about
lifetimes since the compiler inferred most of them. Things only got rough when I
experimented with async and started running into more cryptic borrow checker errors, but
that is a story for another time.</p>
<p>The project evolved alongside my understanding of Rust. The <a href="https://docs.rs/nzb-rs/0.1.0/nzb_rs/">first version</a> of the
parser was a fairly direct port of the <a href="https://github.com/Ravencentric/nzb/blob/v0.3.0/src/nzb/_core.py">Python implementation</a> with an almost identical
API. Even so, the Rust version ended up being several times faster. Some of that came
from Rust itself, but the process also highlighted inefficiencies in the Python
implementation. Taking those lessons back, I was able to remove quite a bit of slower
code and narrow the performance gap. Later versions of the Rust library evolved into a
more idiomatic API. Ironically, the Python implementation was eventually <a href="https://github.com/Ravencentric/nzb/releases/tag/v0.4.0">updated</a> to be
closer to the Rust API wherever it made sense.</p>
<h3 id="rnzb">rnzb</h3>
<p><em>Python bindings to the NZB-rs library - a spec compliant parser for NZB files, written
in Rust</em></p>
<p><a href="https://github.com/Ravencentric/rnzb">[GitHub]</a>
<a href="https://pypi.org/project/rnzb/">[PyPI]</a></p>
<p>A third NZB parser? Yes. At this point I might have a problem.</p>
<p>After writing one in Python and another in Rust, I had a thought: &ldquo;Wouldn&rsquo;t it be cool
to use the Rust implementation directly from Python and get the best of both worlds?&rdquo;
Spoiler alert: yes.</p>
<p>My goal was simple: a drop-in replacement so existing Python code could immediately
benefit from the Rust parser.</p>
<p>After a bit of research I found <a href="https://pyo3.rs/"><code>PyO3</code></a>, which powers Pydantic and many other
Rust-powered Python extensions. This was my first time writing an extension module, but
thankfully <code>PyO3</code> takes care of most of the nitty gritty details and lets me write Rust
like nothing&rsquo;s changed. Honestly, the maintainers have done an amazing job making this
so easy I could hardly believe it. There were a few small hiccups along the way, but the
docs and maintainers were incredibly helpful whenever I had questions.</p>
<p>Once everything worked and the tests passed, the next step was packaging. Wheels are
prebuilt Python packages so users do not have to compile anything during installation.
To build wheels for multiple platforms I used <a href="https://github.com/pypa/cibuildwheel"><code>pypa/cibuildwheel</code></a>, which seems to be
what most projects rely on.</p>
<p>One interesting detail about extension wheels is Python version compatibility. For
example: <code>rnzb-0.6.0-cp314-cp314-win_arm64.whl</code>. The <code>cp314-cp314</code> tag means the wheel
only works on CPython 3.14. On newer versions the installer falls back to the source
distribution and tries to build it locally, which usually fails without a Rust
toolchain.</p>
<p>To avoid releasing new wheels for every Python version, I built the extension against
the <a href="https://docs.python.org/3/c-api/stable.html#limited-c-api">Limited C API</a> (abi3), which <code>PyO3</code> supports. The result looks like this:
<code>rnzb-0.6.0-cp39-abi3-win_amd64.whl</code>. The <code>cp39-abi3</code> tag means the same wheel works on
every CPython version starting from Python 3.9.</p>
<p>So yes, the end result was a third NZB parser. But this one is a little different: it
brings the speed and safety of the Rust implementation to Python while keeping a
familiar drop-in API. You get the Rust parser without changing your code.</p>
<h3 id="atomicwriter">atomicwriter</h3>
<p><em>Cross-platform atomic file writer for all-or-nothing operations.</em></p>
<p><a href="https://github.com/Ravencentric/atomicwriter">[GitHub]</a>
<a href="https://pypi.org/project/atomicwriter/">[PyPI]</a>
<a href="https://ravencentric.cc/atomicwriter/">[Docs]</a></p>
<p>Every now and then I need to write something atomically. There used to be a package for
this, <a href="https://github.com/untitaker/python-atomicwrites"><code>python-atomicwrites</code></a>, but it is unmaintained now.</p>
<p>I am not an expert in file system behavior across different platforms, but I knew of
one: Rust&rsquo;s <a href="https://docs.rs/tempfile/latest/tempfile/"><code>tempfile</code></a> crate. It already implements atomic writes (and a lot more)
across platforms. I also wanted an excuse to use more Rust, so this was a good fit.</p>
<p>Thanks to everything I learned from my previous projects, this one was fairly
straightforward. I wrote a thin wrapper around the Rust library, exposed an idiomatic
Python API, added some tests, and published it with abi3 wheels.</p>
<h3 id="privatebin">PrivateBin</h3>
<p><em>Python library for interacting with PrivateBin&rsquo;s v2 API (PrivateBin &gt;= 1.3) to create,
retrieve, and delete encrypted pastes.</em></p>
<p><a href="https://github.com/Ravencentric/PrivateBin">[GitHub]</a>
<a href="https://pypi.org/project/PrivateBin/">[PyPI]</a>
<a href="https://ravencentric.cc/PrivateBin/">[Docs]</a></p>
<p>I regularly use an instance of <a href="https://privatebin.info/">PrivateBin</a> to temporarily and securely store logs from
various scripts. Initially I used the <a href="https://github.com/Pioverpie/privatebin-api/"><code>PrivateBinAPI</code></a> package. It had incomplete type
hints and I was not a fan of the API, but it worked&hellip; until it <a href="https://github.com/Pioverpie/privatebin-api/issues/12">didn&rsquo;t</a>.</p>
<p>So I set out to write one from scratch.</p>
<p>One of the defining features of PrivateBin is that it never actually sees your paste.
Everything is encrypted client-side before being sent to the server, which only stores
and returns the encrypted blob, so the client has to handle both encryption and
decryption.</p>
<p>Unfortunately (and maybe this was just me being dumb), the PrivateBin documentation did
not help much here. So I started digging through the source code of <a href="https://github.com/Pioverpie/privatebin-api/"><code>PrivateBinAPI</code></a> to
figure out how it handled things.</p>
<p>Turns out&hellip; it doesn&rsquo;t. It depends on another library, <a href="https://github.com/r4sas/PBinCLI"><code>PBinCLI</code></a>, to actually do the
talking to PrivateBin.</p>
<p>So I went and read that code instead. After a few hours of trial, error, and carefully
extracting pieces of logic, I eventually isolated the core encryption and decryption
routines. Once that part was figured out, the rest of the client was fairly
straightforward, and it doesn&rsquo;t depend on another PrivateBin library.</p>
<h3 id="myne">myne</h3>
<p><em>Parser for manga and light novel filenames</em></p>
<p><a href="https://github.com/Ravencentric/myne">[GitHub]</a>
<a href="https://pypi.org/project/myne/">[PyPI]</a>
<a href="https://ravencentric.cc/myne/">[Docs]</a></p>
<p>Manga and light novel filenames are a mess. Well, mostly. The big release groups tend to
follow pretty consistent naming schemes, but everything else is all over the place,
especially Korean manhwa releases. There&rsquo;s no <a href="https://pypi.org/project/anitopy/"><code>anitopy</code></a> equivalent for manga and light
novels, so I ended up writing my own parser to turn them into structured metadata like
title, volume, and chapter.</p>
<p>It&rsquo;s a Python library written in Rust, and under the hood it is entirely regex-based. It
matches the most specific patterns first and removes them from the filename, then moves
on to more ambiguous ones. Whatever is left at the end is treated as the title.</p>
<p>The name &ldquo;myne&rdquo; comes from the main character of <a href="https://j-novel.club/series/ascendance-of-a-bookworm">Ascendance of a Bookworm</a>, who really
loves books. I&rsquo;m still pretty proud of that one.</p>
<p>I&rsquo;ve used it extensively on real-world files, and it has held up well so far.</p>
<h3 id="mkvinfo">mkvinfo</h3>
<p><em>Python library for probing matroska files with mkvmerge</em></p>
<p><a href="https://github.com/Ravencentric/mkvinfo">[GitHub]</a>
<a href="https://pypi.org/project/mkvinfo/">[PyPI]</a>
<a href="https://ravencentric.cc/mkvinfo/">[Docs]</a></p>
<p>I needed a way to introspect MKV files so I could classify them further. <a href="https://mkvtoolnix.download/doc/mkvmerge.html"><code>mkvmerge</code></a>
can already introspect MKV files and return the result as JSON, but consuming that
output directly from Python gets annoying pretty quickly.</p>
<p>This library is basically a thin wrapper around that. It runs <code>mkvmerge -J</code> and turns
the JSON output into typed Python objects so I don&rsquo;t have to deal with it myself.</p>
<h3 id="misaki">misaki</h3>
<p><em>misaki is a fast, asynchronous link checker with optional FlareSolverr support.</em></p>
<p><a href="https://github.com/Ravencentric/misaki">[GitHub]</a>
<a href="https://crates.io/crates/misaki-core">[crates.io: misaki-core]</a>
<a href="https://crates.io/crates/misaki-cli">[crates.io: misaki-cli]</a>
<a href="https://docs.rs/misaki_core/latest/misaki_core/">[Docs]</a></p>
<p>A good friend of mine wanted a link checker with <a href="https://github.com/FlareSolverr/FlareSolverr">FlareSolverr</a> support. A link checker
is a pretty easy project, and it was an excuse to try out async Rust.</p>
<p>This was the first time Rust actually felt like a pain. The errors got quite a bit more
cryptic, and async support still feels somewhat incomplete.</p>
<p>Some patterns that should be simple end up awkward. For example, there is no built-in
way to &ldquo;yield&rdquo; values from async code, so you end up relying on third-party crates like
<a href="https://docs.rs/async-stream/latest/async_stream/"><code>async-stream</code></a> to emulate async generators. There is also no async drop, which makes
cleaning up async resources harder than it should be.</p>
<p>It&rsquo;s not all bad though. After simply <code>.clone()</code>ing my issues away, I ended up with a
really fast link checker.</p>
<h2 id="2026">2026</h2>
<h3 id="ravencentricgithubio">ravencentric.github.io</h3>
<p><a href="https://github.com/Ravencentric/ravencentric.github.io">[GitHub]</a></p>
<p>Well, that&rsquo;s this site. My apex domain had been sitting unused for a while (my project
docs already live at <code>$domain/$project</code>), and a home page was pretty much the only thing
I could put here, so that&rsquo;s what it is.</p>
<p>It&rsquo;s a static site built with <a href="https://github.com/gohugoio/hugo">Hugo</a> and hosted on <a href="https://docs.github.com/en/pages">GitHub Pages</a>. The theme is
<a href="https://github.com/clente/hugo-bearcub">clente/hugo-bearcub</a> because it&rsquo;s simple, does everything I need (pretty codeblocks),
is JavaScript-free, and comes in under 10KB.</p>
<p>There&rsquo;s not much else to add here since I didn&rsquo;t really do anything beyond taking some
off-the-shelf pieces and putting them together.</p>
]]></content:encoded></item></channel></rss>