<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged python</title>
<link>https://coderlegion.com/tag/python</link>
<description>Powered by Question2Answer</description>
<item>
<title>[OC] I built a free, open-source HIPAA DICOM anonymizer for medical imaging</title>
<link>https://coderlegion.com/33082/oc-i-built-a-free-open-source-hipaa-dicom-anonymizer-for-medical-imaging</link>
<description>&lt;p&gt;I just released &lt;strong&gt;dicom-clean&lt;/strong&gt; — an open-source CLI tool for HIPAA-compliant DICOM de-identification. Here's how it works and why I built it.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Medical imaging files (CT scans, MRIs, X-rays) use a format called &lt;strong&gt;DICOM&lt;/strong&gt;, which contains two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Metadata&lt;/strong&gt; — patient name, birth date, MRN, hospital, referring physician, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pixel data&lt;/strong&gt; — the actual image&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When hospitals want to share DICOM data for research, AI training, or vendor evaluation, they must remove the patient identifiers first — this is required by HIPAA Safe Harbor (45 CFR §164.514(b)(2)).&lt;/p&gt;
&lt;p&gt;But existing options are problematic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Commercial tools&lt;/strong&gt; (RSNA CTP, DicomCleaner) cost $5,000+/year&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud solutions&lt;/strong&gt; upload patient data → HIPAA violation risk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DCMTK / GDCM&lt;/strong&gt; work but have steep learning curves (C++ toolchain, complex syntax)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I built &lt;strong&gt;dicom-clean&lt;/strong&gt; to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;100% offline&lt;/strong&gt; (air-gapped compatible)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Simple CLI&lt;/strong&gt; (one command)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero-install&lt;/strong&gt; (standalone &lt;code&gt;.exe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Free and open source&lt;/strong&gt; (AGPL-3.0)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h3&gt;
&lt;h4 id=&quot;1-strips-all-18-hipaa-patient-identifiers&quot;&gt;1. Strips All 18 HIPAA Patient Identifiers&lt;/h4&gt;
&lt;p&gt;The tool blanks out 40+ scalar PHI tags and deletes 8 sequences that may carry nested PHI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;HIPAA_TAGS_TO_BLANK = (
    (0x0010, 0x0010),  # PatientName
    (0x0010, 0x0020),  # PatientID
    (0x0010, 0x0030),  # PatientBirthDate
    (0x0010, 0x1040),  # PatientAddress
    (0x0008, 0x0080),  # InstitutionName
    (0x0008, 0x0090),  # ReferringPhysicianName
    # ... 40+ more
)

HIPAA_TAGS_TO_REMOVE = (
    (0x0008, 0x1110),  # ReferencedStudySequence
    (0x0008, 0x1115),  # ReferencedSeriesSequence
    # ... 6 more
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; I &lt;strong&gt;blank&lt;/strong&gt; the scalar tags (preserving tag + VR) rather than deleting them, so downstream PACS and DICOM viewers don't break when they expect those fields.&lt;/p&gt;
&lt;h4 id=&quot;2-deterministic-uid-pseudonymization&quot;&gt;2. Deterministic UID Pseudonymization&lt;/h4&gt;
&lt;p&gt;DICOM uses &lt;strong&gt;UIDs&lt;/strong&gt; (Unique Identifiers) to link files together. A study has a &lt;code&gt;StudyInstanceUID&lt;/code&gt;, each series has a &lt;code&gt;SeriesInstanceUID&lt;/code&gt;, and each image has a &lt;code&gt;SOPInstanceUID&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you just replace these with random UUIDs, you lose the relationships between files — a PACS server won't be able to group images from the same study.&lt;/p&gt;
&lt;p&gt;My solution: &lt;strong&gt;SHA-256 with an optional salt&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def pseudonymize_uid(original_uid: str, salt: str = &quot;&quot;) -&amp;gt; str:
    &quot;&quot;&quot;Deterministically pseudonymize a DICOM UID.&quot;&quot;&quot;
    if not original_uid:
        return original_uid
    digest = hashlib.sha256(
        (salt + str(original_uid)).encode(&quot;utf-8&quot;)
    ).hexdigest()
    number = int(digest[:32], 16)  # 128 bits
    return f&quot;2.25.{number}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deterministic&lt;/strong&gt; — Same input + salt → same output, always&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reversible relationships&lt;/strong&gt; — All files from one study get consistent pseudonyms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-reversible&lt;/strong&gt; — Without the salt, you can't reverse it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid format&lt;/strong&gt; — Output is a proper DICOM UID (&lt;code&gt;2.25.&amp;lt;128-bit-int&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-image-integrity&quot;&gt;3. Image Integrity&lt;/h4&gt;
&lt;p&gt;The pixel data is &lt;strong&gt;never touched&lt;/strong&gt;. Same for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Modality&lt;/code&gt; (CT, MR, US, etc.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PixelData&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransferSyntaxUID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PhotometricInterpretation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Rows&lt;/code&gt; / &lt;code&gt;Columns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BitsAllocated&lt;/code&gt; / &lt;code&gt;BitsStored&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PixelSpacing&lt;/code&gt; / &lt;code&gt;SliceThickness&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Window/Level tags&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the image looks identical in any DICOM viewer — only the metadata changes.&lt;/p&gt;
&lt;h4 id=&quot;4-date-reduction&quot;&gt;4. Date Reduction&lt;/h4&gt;
&lt;p&gt;HIPAA permits keeping the year but not the month/day. So:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;Before: StudyDate = &quot;20250315&quot;
After:  StudyDate = &quot;20250101&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;pydicom&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;DICOM parsing &amp;amp; writing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typer&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;CLI framework&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rich&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Terminal UI (panels, progress bars, tables)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PyInstaller&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Standalone &lt;code&gt;.exe&lt;/code&gt; build&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;what-it-looks-like&quot;&gt;What It Looks Like&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ dicom-clean sanitize patient.dcm output\ --salt &quot;my-secret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output shows a live progress bar and a colored summary table:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Total Files Scanned: 1,250&lt;/li&gt;
&lt;li&gt;Successfully Anonymized: 1,248&lt;/li&gt;
&lt;li&gt;Skipped (Non-DICOM): 2&lt;/li&gt;
&lt;li&gt;Errors / Failed: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;directory-batch-processing&quot;&gt;Directory Batch Processing&lt;/h3&gt;
&lt;p&gt;The tool recursively processes directories and preserves the original subfolder structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ dicom-clean sanitize C:\Hospital_Data C:\Clean_Output --salt &quot;hospital-2025&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;robustness&quot;&gt;Robustness&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Non-DICOM files are &lt;strong&gt;skipped with a warning&lt;/strong&gt; instead of crashing&lt;/li&gt;
&lt;li&gt;Per-file errors are &lt;strong&gt;collected into a summary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;try/except&lt;/code&gt; for &lt;code&gt;InvalidDicomError&lt;/code&gt;, &lt;code&gt;OSError&lt;/code&gt;, &lt;code&gt;IOError&lt;/code&gt;, and generic exceptions&lt;/li&gt;
&lt;li&gt;Exit code &lt;code&gt;1&lt;/code&gt; if any file failed, &lt;code&gt;0&lt;/code&gt; otherwise&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;h4 id=&quot;windows-standalone-no-python-required-&quot;&gt;Windows Standalone (No Python Required)&lt;/h4&gt;
&lt;p&gt;Download &lt;code&gt;dicom-clean.exe&lt;/code&gt; from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean/releases&quot;&gt;Releases page&lt;/a&gt; and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;dicom-clean.exe sanitize input.dcm output\ --salt &quot;my-secret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;from-source&quot;&gt;From Source&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/kaveen-ai/dicom-clean.git
cd dicom-clean
pip install -r requirements.txt
python anonymizer.py --help
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;design-tradeoffs&quot;&gt;Design Tradeoffs&lt;/h3&gt;
&lt;p&gt;Some decisions I made and why:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. SHA-256 hashing vs random UUIDs for UIDs&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Hash preserves relationships across files&lt;/li&gt;
&lt;li&gt;✅ Deterministic (reproducible)&lt;/li&gt;
&lt;li&gt;❌ Requires salt management for security&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Blank tags vs delete tags&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Blanking preserves tag structure (PACS-safe)&lt;/li&gt;
&lt;li&gt;✅ Downstream tools don't break&lt;/li&gt;
&lt;li&gt;❌ Leaves an empty tag with metadata footprint&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. AGPL-3.0 + Commercial dual-license&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Free for open-source use&lt;/li&gt;
&lt;li&gt;✅ Commercial license funds development&lt;/li&gt;
&lt;li&gt;❌ AGPL can be a dealbreaker for some hospitals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Single-file CLI vs library-first&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Standalone &lt;code&gt;.exe&lt;/code&gt; for non-technical users&lt;/li&gt;
&lt;li&gt;✅ The core &lt;code&gt;DicomAnonymizer&lt;/code&gt; class is importable as a library&lt;/li&gt;
&lt;li&gt;✅ CLI is just a thin Typer wrapper&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-dicomanonymizer-class&quot;&gt;The &lt;code&gt;DicomAnonymizer&lt;/code&gt; Class&lt;/h3&gt;
&lt;p&gt;If you want to use the engine programmatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from anonymizer import DicomAnonymizer

anonymizer = DicomAnonymizer(
    salt=&quot;my-hospital-secret&quot;,
    remove_private_tags=True,
    reduce_dates_to_year=True,
)

summary = anonymizer.anonymize(
    input_path=&quot;patient.dcm&quot;,
    output_path=&quot;output/&quot;,
)

print(f&quot;Anonymized {summary.anonymized} files&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UID pseudonymization&lt;/li&gt;
&lt;li&gt;HIPAA tag blanking&lt;/li&gt;
&lt;li&gt;Private tag removal&lt;/li&gt;
&lt;li&gt;Date reduction&lt;/li&gt;
&lt;li&gt;File meta sanitization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;I'm planning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions CI&lt;/strong&gt; — test matrix across Python 3.10-3.13&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyPI publication&lt;/strong&gt; — &lt;code&gt;pip install dicom-clean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker image&lt;/strong&gt; — for hospital IT deployments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pixel OCR redaction&lt;/strong&gt; — for burned-in PHI in images (commercial edition)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;feedback-welcome&quot;&gt;Feedback Welcome&lt;/h3&gt;
&lt;p&gt;I'd love feedback from anyone working with DICOM data, healthcare IT, or medical imaging.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Star on GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean&quot;&gt;github.com/kaveen-ai/dicom-clean&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report bugs:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean/issues&quot;&gt;Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean/discussions&quot;&gt;GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What would you like to see next?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;healthcare&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt; &lt;code&gt;dicom&lt;/code&gt; &lt;code&gt;privacy&lt;/code&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33082/oc-i-built-a-free-open-source-hipaa-dicom-anonymizer-for-medical-imaging</guid>
<pubDate>Fri, 30 Oct 2026 10:49:57 +0000</pubDate>
</item>
<item>
<title>Building an ATS Resume Scorer API with FastAPI, Celery, and Docker</title>
<link>https://coderlegion.com/33059/building-an-ats-resume-scorer-api-with-fastapi-celery-and-docker</link>
<description>&lt;p&gt;Most resumes are filtered by ATS software before a human ever reads them. This project is a backend API that checks how well a resume matches a job description and suggests how to improve it.&lt;/p&gt;
&lt;h3 id=&quot;what-it-does-1&quot;&gt;What it does&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PDF resume parsing:&lt;/strong&gt; extracts text from uploaded resumes using pdfplumber&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ATS keyword matching:&lt;/strong&gt; compares the resume against a job description and calculates a match score&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart suggestions:&lt;/strong&gt; generates actionable tips to improve the resume for a specific role&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT authentication:&lt;/strong&gt; secured endpoints with role-based access for Job Seekers and Recruiters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch scoring:&lt;/strong&gt; recruiters can score many resumes at once, handled in the background by Celery + Redis&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;tech-stack-1&quot;&gt;Tech stack&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Technology&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Backend &lt;/td&gt;&lt;td&gt;Python, FastAPI&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Auth &lt;/td&gt;&lt;td&gt;JWT (python-jose, passlib)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Database &lt;/td&gt;&lt;td&gt;MySQL 8.0, SQLAlchemy ORM, Alembic&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Task queue &lt;/td&gt;&lt;td&gt;Celery + Redis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PDF parsing &lt;/td&gt;&lt;td&gt;pdfplumber&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DevOps &lt;/td&gt;&lt;td&gt;Docker, Docker Compose, AWS EC2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CI/CD &lt;/td&gt;&lt;td&gt;GitHub Actions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Testing &lt;/td&gt;&lt;td&gt;Pytest&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;design-highlights&quot;&gt;Design highlights&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Background processing:&lt;/strong&gt; batch scoring runs through a Celery queue, so the API stays responsive while many resumes are processed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role-based access:&lt;/strong&gt; Job Seekers and Recruiters get separate permissions on the same API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducible setup:&lt;/strong&gt; the whole stack (API, MySQL, Redis) starts with a single Docker Compose command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated deployment:&lt;/strong&gt; a GitHub Actions pipeline deploys automatically to AWS EC2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database migrations:&lt;/strong&gt; schema changes are managed with Alembic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;source-code&quot;&gt;Source code&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/priyadharshiny13/ats-resume-scorer&quot;&gt;https://github.com/priyadharshiny13/ats-resume-scorer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Feedback is welcome. If you were using a tool like this, what would matter more: a more accurate match score or more detailed improvement suggestions?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33059/building-an-ats-resume-scorer-api-with-fastapi-celery-and-docker</guid>
<pubDate>Fri, 30 Oct 2026 07:57:26 +0000</pubDate>
</item>
<item>
<title>How to Format SQL Queries in Python: Best Practices, Gotchas, and Real-World Examples</title>
<link>https://coderlegion.com/33002/how-to-format-sql-queries-in-python-best-practices-gotchas-and-real-world-examples</link>
<description>&lt;p&gt;&lt;em&gt;Stop writing SQL strings that look like a ransom note. Here's how to write queries that are readable, safe, and maintainable.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-with-good-enough-sql-formatting&quot;&gt;The Problem With &quot;Good Enough&quot; SQL Formatting&lt;/h3&gt;
&lt;p&gt;Most Python developers start here:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;user_id = 5
query = &quot;SELECT * FROM users WHERE id = &quot; + str(user_id)
cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. Until it doesn't — and when it breaks, it breaks &lt;em&gt;badly&lt;/em&gt;: SQL injection, cryptic errors from mismatched types, and queries that take 45 minutes to debug at 2am. Let's fix that, permanently.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-never-concatenate-user-input-use-parameterized-queries&quot;&gt;1. Never Concatenate User Input — Use Parameterized Queries&lt;/h3&gt;
&lt;p&gt;This is rule #1 and it's non-negotiable.&lt;/p&gt;
&lt;h4 id=&quot;-the-wrong-way-sql-injection-waiting-to-happen-&quot;&gt;❌ The Wrong Way (SQL Injection Waiting to Happen)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;username = request.args.get(&quot;username&quot;)  # Could be: ' OR '1'='1
query = f&quot;SELECT * FROM users WHERE username = '{username}'&quot;
cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;username&lt;/code&gt; is &lt;code&gt;' OR '1'='1&lt;/code&gt;, your entire users table just got exposed.&lt;/p&gt;
&lt;h4 id=&quot;-the-right-way-parameterized-queries&quot;&gt;✅ The Right Way: Parameterized Queries&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;username = request.args.get(&quot;username&quot;)

# psycopg2 (PostgreSQL)
cursor.execute(&quot;SELECT * FROM users WHERE username = %s&quot;, (username,))

# sqlite3
cursor.execute(&quot;SELECT * FROM users WHERE username = ?&quot;, (username,))

# SQLAlchemy Core
from sqlalchemy import text
result = conn.execute(text(&quot;SELECT * FROM users WHERE username = :name&quot;), {&quot;name&quot;: username})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database driver handles escaping. You never touch it. This pattern is immune to SQL injection by design.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Note the trailing comma in &lt;code&gt;(username,)&lt;/code&gt;. Without it, Python treats the string as an iterable and passes each &lt;em&gt;character&lt;/em&gt; as a separate parameter. This is one of the most common beginner bugs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 💥 Bug: passes ('a', 'l', 'i', 'c', 'e') instead of ('alice',)
cursor.execute(&quot;SELECT * FROM users WHERE username = %s&quot;, (username))

# ✅ Correct: single-element tuple
cursor.execute(&quot;SELECT * FROM users WHERE username = %s&quot;, (username,))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-multi-line-queries-triple-quotes-consistent-indentation&quot;&gt;2. Multi-Line Queries: Triple Quotes + Consistent Indentation&lt;/h3&gt;
&lt;p&gt;For anything longer than one clause, use triple-quoted strings. They're readable, diffable, and don't require escape characters.&lt;/p&gt;
&lt;h4 id=&quot;-string-concatenation-chaos&quot;&gt;❌ String Concatenation Chaos&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;query = &quot;SELECT u.id, u.name, o.total &quot; + \
        &quot;FROM users u &quot; + \
        &quot;JOIN orders o ON u.id = o.user_id &quot; + \
        &quot;WHERE o.total &amp;gt; 100&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-triple-quoted-sql&quot;&gt;✅ Triple-Quoted SQL&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;query = &quot;&quot;&quot;
    SELECT
        u.id,
        u.name,
        u.email,
        o.total,
        o.created_at
    FROM users u
    JOIN orders o
        ON u.id = o.user_id
    WHERE
        o.total &amp;gt; 100
        AND u.active = TRUE
    ORDER BY o.created_at DESC
    LIMIT 50
&quot;&quot;&quot;

cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Style conventions that matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL keywords in &lt;code&gt;UPPERCASE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;One clause per line (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Indent column lists by 4 spaces&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;ON&lt;/code&gt; conditions on their own line, indented under &lt;code&gt;JOIN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-dynamic-queries-building-them-safely&quot;&gt;3. Dynamic Queries: Building Them Safely&lt;/h3&gt;
&lt;p&gt;Real applications need dynamic filters. Here's how to do it without string concatenation.&lt;/p&gt;
&lt;h4 id=&quot;scenario-a-search-endpoint-with-optional-filters&quot;&gt;Scenario: A search endpoint with optional filters&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_orders(user_id: int, status: str = None, min_total: float = None):
    base_query = &quot;&quot;&quot;
        SELECT
            o.id,
            o.status,
            o.total,
            o.created_at
        FROM orders o
        WHERE o.user_id = %s
    &quot;&quot;&quot;
    params = [user_id]
    conditions = []

    if status:
        conditions.append(&quot;AND o.status = %s&quot;)
        params.append(status)

    if min_total is not None:
        conditions.append(&quot;AND o.total &amp;gt;= %s&quot;)
        params.append(min_total)

    full_query = base_query + &quot;\n        &quot;.join(conditions)

    cursor.execute(full_query, params)
    return cursor.fetchall()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the &lt;strong&gt;accumulator pattern&lt;/strong&gt;: build a list of clauses and params in parallel, then join them. The number of &lt;code&gt;%s&lt;/code&gt; placeholders always matches &lt;code&gt;len(params)&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Never dynamically interpolate column names or table names with parameters — the DB driver only parameterizes &lt;em&gt;values&lt;/em&gt;, not identifiers. If you need a dynamic column name, whitelist it explicitly:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ALLOWED_SORT_COLUMNS = {&quot;created_at&quot;, &quot;total&quot;, &quot;status&quot;}

def get_orders_sorted(sort_by: str):
    if sort_by not in ALLOWED_SORT_COLUMNS:
        raise ValueError(f&quot;Invalid sort column: {sort_by}&quot;)

    # Now safe to interpolate — it came from our whitelist, not user input
    query = f&quot;&quot;&quot;
        SELECT id, status, total, created_at
        FROM orders
        ORDER BY {sort_by} DESC
    &quot;&quot;&quot;
    cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-in-clauses-the-tricky-one&quot;&gt;4. IN Clauses: The Tricky One&lt;/h3&gt;
&lt;p&gt;Parameterizing &lt;code&gt;IN (...)&lt;/code&gt; trips up almost everyone the first time.&lt;/p&gt;
&lt;h4 id=&quot;-this-won-t-work&quot;&gt;❌ This Won't Work&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ids = [1, 2, 3, 4]
cursor.execute(&quot;SELECT * FROM users WHERE id IN (%s)&quot;, (ids,))
# Error: query expects 1 placeholder, got a list
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-generate-placeholders-dynamically&quot;&gt;✅ Generate Placeholders Dynamically&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ids = [1, 2, 3, 4]

# sqlite3
placeholders = &quot;, &quot;.join(&quot;?&quot; * len(ids))
query = f&quot;SELECT * FROM users WHERE id IN ({placeholders})&quot;
cursor.execute(query, ids)

# psycopg2 — uses %s for each
placeholders = &quot;, &quot;.join([&quot;%s&quot;] * len(ids))
query = f&quot;SELECT * FROM users WHERE id IN ({placeholders})&quot;
cursor.execute(query, ids)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-sqlalchemy-makes-this-trivial&quot;&gt;✅ SQLAlchemy makes this trivial&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import select, bindparam

stmt = select(users_table).where(users_table.c.id.in_([1, 2, 3, 4]))
result = conn.execute(stmt)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-use-sqlalchemy-core-for-complex-queries&quot;&gt;5. Use SQLAlchemy Core for Complex Queries&lt;/h3&gt;
&lt;p&gt;When your raw SQL starts looking like a maze, SQLAlchemy Core gives you composable, Pythonic query building without the full ORM overhead.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import create_engine, select, func, and_, desc
from sqlalchemy import Table, MetaData

engine = create_engine(&quot;postgresql://user:pass@localhost/mydb&quot;)
meta = MetaData()
meta.reflect(bind=engine)

orders = meta.tables[&quot;orders&quot;]
users = meta.tables[&quot;users&quot;]

stmt = (
    select(
        users.c.id,
        users.c.name,
        func.sum(orders.c.total).label(&quot;lifetime_value&quot;),
        func.count(orders.c.id).label(&quot;order_count&quot;),
    )
    .join(orders, users.c.id == orders.c.user_id)
    .where(
        and_(
            orders.c.status == &quot;completed&quot;,
            orders.c.created_at &amp;gt;= &quot;2024-01-01&quot;,
        )
    )
    .group_by(users.c.id, users.c.name)
    .having(func.sum(orders.c.total) &amp;gt; 500)
    .order_by(desc(&quot;lifetime_value&quot;))
    .limit(20)
)

with engine.connect() as conn:
    results = conn.execute(stmt).fetchall()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compiles to safe, parameterized SQL automatically. You can also print the generated SQL for debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(stmt.compile(dialect=engine.dialect, compile_kwargs={&quot;literal_binds&quot;: True}))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-formatting-for-logging-and-debugging&quot;&gt;6. Formatting for Logging and Debugging&lt;/h3&gt;
&lt;p&gt;When you need to log a query (for debugging, audit trails, etc.), format it cleanly — but &lt;em&gt;never&lt;/em&gt; log with real user data embedded.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import logging
import re

logger = logging.getLogger(__name__)

def log_query(query: str, params: tuple):
    # Normalize whitespace for clean log output
    clean_query = re.sub(r'\s+', ' ', query.strip())
    logger.debug(&quot;Executing SQL: %s | Params: %s&quot;, clean_query, params)

log_query(&quot;&quot;&quot;
    SELECT id, name
    FROM users
    WHERE active = %s
&quot;&quot;&quot;, (True,))

# Output: Executing SQL: SELECT id, name FROM users WHERE active = %s | Params: (True,)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Don't use &lt;code&gt;cursor.mogrify()&lt;/code&gt; (psycopg2) to log queries with real values in production — it defeats the purpose of parameterization from an audit standpoint, and if logs are exfiltrated, you've exposed real data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-query-constants-sql-as-module-level-variables&quot;&gt;7. Query Constants: SQL as Module-Level Variables&lt;/h3&gt;
&lt;p&gt;For queries used across your codebase, define them as constants at the top of your module. This makes them findable, testable, and easy to update.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# queries.py

GET_USER_BY_ID = &quot;&quot;&quot;
    SELECT id, name, email, created_at
    FROM users
    WHERE id = %s
      AND active = TRUE
&quot;&quot;&quot;

GET_USER_ORDERS = &quot;&quot;&quot;
    SELECT
        o.id,
        o.status,
        o.total,
        o.created_at
    FROM orders o
    WHERE o.user_id = %s
    ORDER BY o.created_at DESC
&quot;&quot;&quot;

INSERT_ORDER = &quot;&quot;&quot;
    INSERT INTO orders (user_id, total, status, created_at)
    VALUES (%s, %s, %s, NOW())
    RETURNING id
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Usage in your service layer
from queries import GET_USER_BY_ID, GET_USER_ORDERS

def get_user_with_orders(user_id: int):
    cursor.execute(GET_USER_BY_ID, (user_id,))
    user = cursor.fetchone()

    cursor.execute(GET_USER_ORDERS, (user_id,))
    orders = cursor.fetchall()

    return user, orders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes SQL grep-able, version-controllable, and reviewable in PRs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-gotchas-quick-reference&quot;&gt;8. Gotchas Quick Reference&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Gotcha &lt;/th&gt;&lt;th&gt;What Goes Wrong &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;(value)&lt;/code&gt; vs &lt;code&gt;(value,)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;String unpacked as characters &lt;/td&gt;&lt;td&gt;Always use trailing comma for single-element tuples&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;f-string with user input &lt;/td&gt;&lt;td&gt;SQL injection &lt;/td&gt;&lt;td&gt;Use parameterized queries&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic &lt;code&gt;IN (...)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Wrong number of placeholders &lt;/td&gt;&lt;td&gt;Generate placeholders programmatically&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic column names &lt;/td&gt;&lt;td&gt;Parameters don't work for identifiers &lt;/td&gt;&lt;td&gt;Whitelist allowed column names&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Logging with real values &lt;/td&gt;&lt;td&gt;Data exposure in logs &lt;/td&gt;&lt;td&gt;Log query template + params separately&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;None&lt;/code&gt; in params &lt;/td&gt;&lt;td&gt;NULL comparison breaks &lt;/td&gt;&lt;td&gt;Use &lt;code&gt;IS NULL&lt;/code&gt; in SQL, not &lt;code&gt;= NULL&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# None/NULL gotcha
user_role = None

# ❌ Won't match any rows — SQL NULL != NULL
cursor.execute(&quot;SELECT * FROM users WHERE role = %s&quot;, (user_role,))

# ✅ Use IS NULL when the value might be None
if user_role is None:
    cursor.execute(&quot;SELECT * FROM users WHERE role IS NULL&quot;)
else:
    cursor.execute(&quot;SELECT * FROM users WHERE role = %s&quot;, (user_role,))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Recommended Approach&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Simple parameterized query &lt;/td&gt;&lt;td&gt;&lt;code&gt;cursor.execute(sql, (val,))&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Multi-line query &lt;/td&gt;&lt;td&gt;Triple-quoted string with clause-per-line formatting&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic filters &lt;/td&gt;&lt;td&gt;Accumulator pattern (list of clauses + params)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; clause &lt;/td&gt;&lt;td&gt;Generate placeholders: &lt;code&gt;&quot;, &quot;.join([&quot;%s&quot;] * len(ids))&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complex joins/aggregates &lt;/td&gt;&lt;td&gt;SQLAlchemy Core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reusable queries &lt;/td&gt;&lt;td&gt;Module-level string constants&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic column names &lt;/td&gt;&lt;td&gt;Whitelist validation + f-string&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The core rule is simple: &lt;strong&gt;SQL structure is yours to format; SQL values are the driver's job to escape.&lt;/strong&gt; Internalize that boundary and most of these best practices follow naturally.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found a gotcha I missed? Drop it in the comments — SQL has a way of surprising everyone eventually.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33002/how-to-format-sql-queries-in-python-best-practices-gotchas-and-real-world-examples</guid>
<pubDate>Thu, 29 Oct 2026 16:30:33 +0000</pubDate>
</item>
<item>
<title>Learn Any ML Library by Rebuilding It: NumPy, pandas, scikit-learn, PyTorch and sentence-transformer</title>
<link>https://coderlegion.com/32903/learn-any-library-rebuilding-numpy-pandas-scikit-learn-pytorch-and-sentence-transformer</link>
<description>&lt;h3 id=&quot;a-introduction&quot;&gt;A. Introduction&lt;/h3&gt;
&lt;h4 id=&quot;1-abstract&quot;&gt;1. Abstract&lt;/h4&gt;
&lt;p&gt;Some people learning machine learning reach a stage where they can run code copied from tutorials but can't write it themselves. This post presents a No Shortcuts addendum which argues that one cause is the way libraries are usually learned: function by function, from the outside, without the small data model and design conventions each library is built on. For NumPy, pandas, scikit-learn, PyTorch and sentence-transformers, the addendum identifies that core, rebuilds it as a miniature in plain Python, and checks the miniature against the real library, about 5,000 checks in all, rerun by CI on every change. Here you get the argument, the method, and one real result from each library. The full text, about 32,000 words with every miniature and its tests, lives in its own branch of the course repository. Every output shown was produced by running the code.&lt;/p&gt;
&lt;h4 id=&quot;2-the-question&quot;&gt;2. The question&lt;/h4&gt;
&lt;p&gt;Someone on r/learnmachinelearning asked a question a few days ago that I suspect more people have than say out loud. Paraphrasing: &lt;em&gt;&quot;I copy the code from the labs and tutorials and it works, but I can't write it myself. When I see &lt;code&gt;fit()&lt;/code&gt; and &lt;code&gt;predict()&lt;/code&gt; or &lt;code&gt;model.encode(...)&lt;/code&gt; I understand what they do in that example, and I'm lost as soon as the situation is a little different.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I know that feeling very well. My first real ML job had very little to do with the ML I had studied. They gave me a table with 400 million rows and half the labels broken, the pipeline blew up every five minutes, and the courses I had paid for (three of them, my wallet still remembers xD) were useless for all of it. They had taught me which function to call. None of them had taught me what the function was doing, so the moment the data stopped looking like the tutorial's, I was copying code I didn't understand into a problem it wasn't written for.&lt;/p&gt;
&lt;p&gt;The answer I ended up writing is far too long for one post (around 32,000 words), so it became the first addendum to my course, and this post is the door to it.&lt;/p&gt;
&lt;h4 id=&quot;3-where-the-copy-paste-phase-comes-from&quot;&gt;3. Where the copy-paste phase comes from&lt;/h4&gt;
&lt;p&gt;A common way to learn a library, and the way I learned my first ones, is from the outside, one function at a time. You see &lt;code&gt;df.groupby(&quot;city&quot;).mean()&lt;/code&gt; in a tutorial, it works, you remember it. You see &lt;code&gt;StandardScaler().fit_transform(X)&lt;/code&gt;, it works, you remember that too. After a few months you have a long list of incantations and no model of what's underneath, so every new situation looks like a new incantation you haven't seen yet.&lt;/p&gt;
&lt;p&gt;The thing is that each of these libraries is a &lt;strong&gt;small set of ideas&lt;/strong&gt;, a data model plus a handful of design conventions, with thousands of functions built on top:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NumPy is one idea: an array is a block of memory plus four numbers that say how to read it. Slicing, transposing, broadcasting, reshaping and &quot;why did my original array change?&quot; all come out of it.&lt;/li&gt;
&lt;li&gt;pandas adds one more: every column carries an index, and operations line up by label, not by position. Most pandas surprises are that alignment doing its job.&lt;/li&gt;
&lt;li&gt;scikit-learn is a contract. Hyperparameters go in the constructor, &lt;code&gt;fit&lt;/code&gt; learns from data and stores the result in attributes that end with an underscore, &lt;code&gt;predict&lt;/code&gt; and &lt;code&gt;transform&lt;/code&gt; use what was learned. Pipelines and cross-validation are built on that contract.&lt;/li&gt;
&lt;li&gt;PyTorch is tensors (NumPy's idea again) plus a recorder that writes down every operation so it can compute gradients backwards, plus &lt;code&gt;nn.Module&lt;/code&gt;, which is mostly a clever way of collecting parameters.&lt;/li&gt;
&lt;li&gt;sentence-transformers is a tokenizer, a transformer, an average and a normalization. &lt;code&gt;model.encode()&lt;/code&gt; is about fifteen lines once you know that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you know the idea, the documentation turns from a wall of functions into a list of things whose behavior you can mostly guess, and that's what lets you write code instead of copying it.&lt;/p&gt;
&lt;h4 id=&quot;4-and-now-the-code-comes-from-an-ai&quot;&gt;4. And now the code comes from an AI&lt;/h4&gt;
&lt;p&gt;There's a second cause, and it's newer. A few years ago the code you copied came from a tutorial or a Stack Overflow answer, and at least you had to find it, read it a bit, and adapt it. Now you ask an assistant, it hands you thirty lines that run, you paste them, and nobody ever types a single character of them. The code works, so it feels like progress, and if what you wanted was to learn the library, it mostly isn't.&lt;/p&gt;
&lt;p&gt;This one has been measured. In a randomized experiment published in January 2026, developers learned a new Python library (Trio, for asynchronous code), some with an AI assistant and some without (&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/2601.20245&quot;&gt;Shen and Tamkin, &quot;How AI Impacts Skill Formation&quot;&lt;/a&gt;; both authors work at an AI company, which is worth knowing when you read it). The group with AI understood the library worse, read and debugged code worse, and wasn't significantly faster on average. Those who handed the whole task to the assistant learned the least. The ones who kept their heads in it (asking for explanations, checking what they got) kept most of the learning. It's one study with 52 people and one library, so don't treat it as a law of nature, but it points the same way as decades of memory research.&lt;/p&gt;
&lt;p&gt;That research has a name for the effect: the &lt;strong&gt;generation effect&lt;/strong&gt;. You remember what you produce much better than what you only read. It was described in 1978 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1037/0278-7393.4.6.592&quot;&gt;Slamecka and Graf&lt;/a&gt;), and a meta-analysis of 86 studies later found the advantage again and again, close to half a standard deviation on average (&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.3758/BF03193441&quot;&gt;Bertsch et al., 2007&lt;/a&gt;). Pasting code is reading it, at best. Writing it from memory, and only then checking, is generating it, and typing it sits somewhere in between.&lt;/p&gt;
&lt;p&gt;Nobody can stop you from pasting, and at work you'll probably use assistants a lot, which is fine. But while you're &lt;em&gt;learning&lt;/em&gt; a library, type every character yourself. I'd go further: write some of it with pencil and paper. I know it sounds old-fashioned, and to be honest about the evidence, the famous &quot;the pen beats the laptop&quot; study about note-taking didn't hold up well when others repeated it (&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1007/s10648-019-09468-2&quot;&gt;Morehead, Dunlosky and Rawson, 2019&lt;/a&gt;), so I won't sell you paper as a superpower. What paper does is take away autocomplete, copy-paste and the assistant, so the only way to get the code onto the page is to generate it, which is the part the evidence supports.&lt;/p&gt;
&lt;h4 id=&quot;5-why-this-is-a-no-shortcuts-addendum&quot;&gt;5. Why this is a No Shortcuts addendum&lt;/h4&gt;
&lt;p&gt;No Shortcuts is a free course on data, ML, deep learning and AI engineering that I've been writing (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts&quot;&gt;github.com/BlueRingsLabs/no-shortcuts&lt;/a&gt;). The course rebuilds the &lt;em&gt;algorithms&lt;/em&gt; from scratch before using the libraries: linear models before scikit-learn, an autograd engine before PyTorch, a GPT before Hugging Face. What it takes for granted is the libraries themselves, and that's exactly where the copy-paste phase lives. So the addendum, &lt;em&gt;Learning ML Libraries by Rebuilding Them&lt;/em&gt;, does to the tools what the course does to the algorithms. It isn't part of the course, and you don't need the course to read it.&lt;/p&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Every snippet below was run, and every output is the real output, on Python 3.13.17, NumPy 2.5.4, pandas 3.1.0, scikit-learn 1.9.1, PyTorch 2.15.0 (CPU) and sentence-transformers 6.2.0. On another machine, floating-point results can differ in the last digit or two.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;b-the-method-in-one-page&quot;&gt;B. The method, in one page&lt;/h3&gt;
&lt;h4 id=&quot;1-learn-the-layers-in-a-different-order&quot;&gt;1. Learn the layers in a different order&lt;/h4&gt;
&lt;p&gt;Every serious library can be read at five levels: the problem it solves, its data model (the one or two objects everything else operates on), its conventions (what gets modified in place, how things are named, what comes back), its API (the thousands of functions) and its implementation. Tutorials start at the API, and that's usually where learners stop too. I think that's backwards. The data model and the conventions take an afternoon or two per library, and after that a function you've never seen is much less of a mystery: you already know what kind of object it takes, whether it'll touch your data and what shape its result has, because those are decided by the conventions.&lt;/p&gt;
&lt;h4 id=&quot;2-read-the-right-kind-of-documentation&quot;&gt;2. Read the right kind of documentation&lt;/h4&gt;
&lt;p&gt;A big reason documentation feels overwhelming is reading the wrong kind for what you need. Daniele Procida's &lt;strong&gt;Diátaxis&lt;/strong&gt; framework (&lt;a rel=&quot;nofollow&quot; href=&quot;https://diataxis.fr/&quot;&gt;diataxis.fr&lt;/a&gt;) splits it into four kinds:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Kind &lt;/th&gt;&lt;th&gt;It answers &lt;/th&gt;&lt;th&gt;Read it when&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Tutorial &lt;/td&gt;&lt;td&gt;&quot;Show me, step by step, so I get something working.&quot; &lt;/td&gt;&lt;td&gt;You've never used the library. Once.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;How-to guide &lt;/td&gt;&lt;td&gt;&quot;How do I do this specific thing?&quot; &lt;/td&gt;&lt;td&gt;You have a task and know roughly what you need.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reference &lt;/td&gt;&lt;td&gt;&quot;What exactly does this function take and return?&quot; &lt;/td&gt;&lt;td&gt;You're writing code and need a precise detail.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Explanation &lt;/td&gt;&lt;td&gt;&quot;Why does it work this way? What's the idea?&quot; &lt;/td&gt;&lt;td&gt;You want to understand the library.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Going straight to the reference to &lt;em&gt;learn&lt;/em&gt; is like learning a language from its dictionary. The explanation pages (&quot;Copies and views&quot; in NumPy, &quot;Developing scikit-learn estimators&quot; in scikit-learn, the autograd notes in PyTorch) are where the data model and the conventions live. They're short, and they're the easiest pages to skip.&lt;/p&gt;
&lt;h4 id=&quot;3-build-it-small-check-it-against-the-real-thing&quot;&gt;3. Build it small, check it against the real thing&lt;/h4&gt;
&lt;p&gt;This is the loop every chapter of the addendum follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the core: read the explanation pages until you can say in two sentences what the data model is and what the main conventions are.&lt;/li&gt;
&lt;li&gt;Write a miniature from memory, twenty to a hundred lines of plain Python that implement that core and nothing else. Getting stuck is information.&lt;/li&gt;
&lt;li&gt;Test it against the real library on many random inputs, with the library's own tools when it has them (&lt;code&gt;np.testing&lt;/code&gt;, &lt;code&gt;pandas.testing&lt;/code&gt;, scikit-learn's &lt;code&gt;check_estimator&lt;/code&gt;, &lt;code&gt;torch.autograd.gradcheck&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Read every failure, because each one is something you believed about the library that isn't true.&lt;/li&gt;
&lt;li&gt;Read the real source of whatever you got wrong. Now you know what you're looking for.&lt;/li&gt;
&lt;li&gt;Use the real library on a small project, without a tutorial.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The miniatures are slow and incomplete on purpose, so that you can hold the whole thing in your head. And they pay off when they disagree with the library, as you'll see in section G below.&lt;/p&gt;
&lt;h4 id=&quot;4-type-it-write-it-on-paper-use-the-assistant-as-a-tutor&quot;&gt;4. Type it, write it on paper, use the assistant as a tutor&lt;/h4&gt;
&lt;p&gt;Type everything you study, including the code in this post. Predict what each line prints before you run it. The next day, write the miniature again on paper, from memory, then type it and run the test. And use assistants as a tutor, not as a ghostwriter: &quot;ask me five questions about scikit-learn's estimator contract and don't give me the answers until I've tried&quot; is retrieval practice with infinite questions, while &quot;write the training loop for me&quot; skips exactly the part that builds the skill.&lt;/p&gt;
&lt;p&gt;The full chapter adds introspection (&lt;code&gt;inspect.signature&lt;/code&gt;, &lt;code&gt;__mro__&lt;/code&gt;, reading a library's source), reading a traceback from the bottom up, what to memorize, and how libraries warn you before they change, including why Python hides most of those warnings from your own modules: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/B-a-method-for-learning-any-library.md&quot;&gt;chapter B in the addendum&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;c-numpy-the-bug-that-doesn-t-crash&quot;&gt;C. NumPy: the bug that doesn't crash&lt;/h3&gt;
&lt;p&gt;A NumPy array prints like a nested list, but underneath it's a flat buffer plus a dtype, a shape, strides (how many bytes to jump along each axis) and an offset. Slicing computes new strides over the same buffer, which is why a slice is a view. Broadcasting gives the stretched axis a stride of zero, which is why &lt;code&gt;np.broadcast_to(np.arange(4), (3, 4)).strides&lt;/code&gt; is &lt;code&gt;(0, 8)&lt;/code&gt;. The miniature, &lt;code&gt;MiniArray&lt;/code&gt;, is about 120 lines, and it agrees with NumPy on 805 random cases.&lt;/p&gt;
&lt;p&gt;Once you see broadcasting that way, this bug stops being mysterious. You want to subtract each row's mean from that row:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np

X = np.array([[1., 2., 3.], [10., 20., 30.], [100., 200., 300.]])
print(X.mean(axis=1))
print(X - X.mean(axis=1))                  # wrong, and no error
print(X - X.mean(axis=1, keepdims=True))   # right
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[  2.  20. 200.]
[[  -1.  -18. -197.]
 [   8.    0. -170.]
 [  98.  180.  100.]]
[[  -1.    0.    1.]
 [ -10.    0.   10.]
 [-100.    0.  100.]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;X.mean(axis=1)&lt;/code&gt; has shape &lt;code&gt;(3,)&lt;/code&gt;, and broadcasting aligns shapes on the right, so it's compared with the &lt;em&gt;columns&lt;/em&gt; of &lt;code&gt;X&lt;/code&gt;. With a &lt;code&gt;(5, 3)&lt;/code&gt; matrix NumPy would refuse with an error. With a square one it happily subtracts row 0's mean from column 0, row 1's mean from column 1, and so on: garbage with the right shape, which is how a bug survives into a model.&lt;/p&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;After any reduction you plan to broadcast back (&lt;code&gt;mean&lt;/code&gt;, &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;std&lt;/code&gt; along an axis), use &lt;code&gt;keepdims=True&lt;/code&gt;.&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;The chapter also covers views and copies, why &lt;code&gt;c[c &amp;gt; 2][0] = 99&lt;/code&gt; silently does nothing, &lt;code&gt;einsum&lt;/code&gt; as named loops, integer overflow and the NumPy 2 promotion rules, why vectorized code is 1,400 times faster than a loop here, and random generators: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/C-numpy-in-miniature.md&quot;&gt;chapter C&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;d-pandas-a-join-that-almost-doubles-your-revenue&quot;&gt;D. pandas: a join that almost doubles your revenue&lt;/h3&gt;
&lt;p&gt;pandas is NumPy plus labels. Every binary operation between two Series is an outer join on the index, which is where the NaNs that &quot;appear out of nowhere&quot; come from, and the miniature, &lt;code&gt;MiniSeries&lt;/code&gt;, does it with one dictionary lookup per label. Joins are also where some of the most expensive bugs live, because they don't raise anything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pandas as pd

orders = pd.DataFrame({&quot;customer&quot;: [&quot;ana&quot;, &quot;ana&quot;, &quot;bo&quot;], &quot;amount&quot;: [10, 20, 5]})
emails = pd.DataFrame({&quot;customer&quot;: [&quot;ana&quot;, &quot;ana&quot;, &quot;bo&quot;], &quot;email&quot;: [&quot;a@x&quot;, &quot;ana@y&quot;, &quot;b@x&quot;]})
m = orders.merge(emails, on=&quot;customer&quot;)
print(len(orders), len(m))
print(orders[&quot;amount&quot;].sum(), m[&quot;amount&quot;].sum())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3 5
35 65
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ana has two email addresses, so each of her orders matched twice. Three orders became five rows, and the revenue went from 35 to 65. A model trained on &lt;code&gt;m&lt;/code&gt; sees Ana's orders twice, and a dashboard built on it reports almost double the revenue. Telling pandas what kind of join you think you're doing turns the silence into an error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;orders.merge(emails, on=&quot;customer&quot;, validate=&quot;many_to_one&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The chapter rebuilds alignment, labels versus positions, copy-on-write (the pandas 3 change that broke old tutorials, built as a buffer that counts its readers) and split-apply-combine, and checks it all on 1,700 random cases. And it shows what &lt;code&gt;read_csv&lt;/code&gt; guesses when nobody tells it anything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import io

raw = &quot;&quot;&quot;customer_id,zip,signup,amount,vip
000123,02134,2026-01-05,19.99,yes
000124,10001,2026-01-06,,no
000125,94105,2026-01-07,&quot;1,200.50&quot;,yes
&quot;&quot;&quot;
print(pd.read_csv(io.StringIO(raw)).dtypes)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;customer_id    int64
zip            int64
signup           str
amount           str
vip              str
dtype: object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every single column is a little wrong, and not one of them raised a damn thing. The IDs and the zip codes lost their leading zeros (&quot;02134&quot;, a Boston zip code, is now 2134). &lt;code&gt;amount&lt;/code&gt; is text because one value has a thousands separator, so &lt;code&gt;.sum()&lt;/code&gt; glues the strings together into &lt;code&gt;'19.991,200.50'&lt;/code&gt;. The dates are text too. The fixes (&lt;code&gt;dtype=&lt;/code&gt;, &lt;code&gt;parse_dates=&lt;/code&gt;, &lt;code&gt;thousands=&lt;/code&gt;) and the date trap that &lt;code&gt;dayfirst=True&lt;/code&gt; sets are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/D-pandas-in-miniature.md&quot;&gt;chapter D&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;e-scikit-learn-85-accuracy-on-coin-flips&quot;&gt;E. scikit-learn: 85% accuracy on coin flips&lt;/h3&gt;
&lt;p&gt;scikit-learn is mostly a contract, and you can write estimators that follow it and let scikit-learn grade them. A first, naive linear regression isn't even graded until it inherits from &lt;code&gt;BaseEstimator&lt;/code&gt;, and then it fails 17 of the library's own checks (&lt;code&gt;check_estimator&lt;/code&gt;). After renaming the learned attributes, adding &lt;code&gt;check_is_fitted&lt;/code&gt; and validating the input with &lt;code&gt;validate_data&lt;/code&gt;, the miniature passes all 51. The contract is also what makes pipelines work, and pipelines are what protect you from this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

rng = np.random.default_rng(0)
X = rng.normal(size=(100, 5000))          # 100 samples, 5,000 features of pure noise
y = rng.integers(0, 2, size=100)          # labels that have nothing to do with X

# wrong: choose the 20 &quot;best&quot; features using all the data, then cross-validate
X_selected = SelectKBest(f_classif, k=20).fit_transform(X, y)
wrong = cross_val_score(LogisticRegression(), X_selected, y, cv=5)

# right: the selection is part of the model, so it's refitted inside every fold
model = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression())
right = cross_val_score(model, X, y, cv=5)

print(f&quot;selection outside CV: {wrong.mean():.2f}&quot;)
print(f&quot;selection inside CV:  {right.mean():.2f}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;selection outside CV: 0.85
selection inside CV:  0.48
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The labels are coin flips, and the wrong version reports 85% accuracy. Out of 5,000 noise features, some are correlated with the coin flips by chance in this sample, and the selection found them using all 100 labels, including the ones the cross-validation later pretends not to know. Inside the pipeline, the selection is refitted on each training fold and the score drops to 48%, which is what a coin flip deserves. It's literally a textbook example, the &quot;wrong and right way to do cross-validation&quot; in &lt;a rel=&quot;nofollow&quot; href=&quot;https://hastie.su.domains/ElemStatLearn/&quot;&gt;&lt;em&gt;The Elements of Statistical Learning&lt;/em&gt;, section 7.10.2&lt;/a&gt;, and a survey found leakage in 294 published papers across 17 scientific fields (&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1016/j.patter.2023.100804&quot;&gt;Kapoor and Narayanan, 2023&lt;/a&gt;).&lt;/p&gt;
&lt;div class=&quot;div-red&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Warning:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Anything that learns from data is part of the model and must be fitted only on training data: scalers, imputers, encoders, feature selection, PCA, oversampling. Put it in a &lt;code&gt;Pipeline&lt;/code&gt;.&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;The chapter builds the estimator contract step by step, shows why the constructor must not touch its arguments (&lt;code&gt;get_params&lt;/code&gt; reads your &lt;code&gt;__init__&lt;/code&gt; signature, and &lt;code&gt;clone&lt;/code&gt; trusts it), writes a pipeline and a grid search in a few dozen lines that match scikit-learn's exactly, and handles real tables with &lt;code&gt;ColumnTransformer&lt;/code&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/E-scikit-learn-in-miniature.md&quot;&gt;chapter E&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;f-pytorch-half-a-network-that-never-trains&quot;&gt;F. PyTorch: half a network that never trains&lt;/h3&gt;
&lt;p&gt;A tensor is NumPy's array again, autograd is a tape that records every operation and replays it backwards, and &lt;code&gt;nn.Module&lt;/code&gt; finds your parameters by watching what you assign to it: it overrides &lt;code&gt;__setattr__&lt;/code&gt;. The miniature builds all three on top of NumPy, plus SGD and Adam, and matches PyTorch on 359 checks, gradients and optimizer trajectories included. Knowing how registration works explains this one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch
import torch.nn as nn


class HalfBroken(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = [nn.Linear(4, 4)]          # a plain list: not registered
        self.head = nn.Linear(4, 1)              # registered

    def forward(self, x):
        return self.head(torch.relu(self.hidden[0](x)))


torch.manual_seed(0)
model = HalfBroken()
print([n for n, _ in model.named_parameters()])

before = model.hidden[0].weight.detach().clone()
opt = torch.optim.SGD(model.parameters(), lr=0.1)
x, y = torch.randn(64, 4), torch.randn(64, 1)
for _ in range(50):
    opt.zero_grad()
    loss = ((model(x) - y) ** 2).mean()
    loss.backward()
    opt.step()

print(torch.equal(before, model.hidden[0].weight), model.hidden[0].weight.grad is not None)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;['head.weight', 'head.bias']
True True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hidden layer got gradients (the second &lt;code&gt;True&lt;/code&gt;), but the optimizer was given &lt;code&gt;model.parameters()&lt;/code&gt;, which doesn't include it, so its weights are exactly what they were before training. The loss still goes down a bit, because the head is learning, and nothing crashes. &lt;code&gt;nn.ModuleList&lt;/code&gt; is the fix, and &lt;code&gt;sum(p.numel() for p in model.parameters())&lt;/code&gt; is the one-line check.&lt;/p&gt;
&lt;p&gt;The same model explains why every training loop starts with &lt;code&gt;zero_grad&lt;/code&gt;: a leaf tensor &lt;em&gt;adds&lt;/em&gt; each new gradient to the one it already has. Here's a one-feature linear regression (the true weight is 3 and the bias −2) trained for 30 steps with and without it, showing the loss at steps 0, 9, 19 and 29, then the learned weight and bias:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;with zero_grad    ([9.5716, 2.6276, 0.9376, 0.4191], [1.957, -1.892])
without zero_grad ([9.5716, 6.6228, 8.9934, 8.1757], [0.497, -4.482])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No error, just a model that doesn't learn, and a bug that looks like &quot;bad hyperparameters&quot;.&lt;/p&gt;
&lt;p&gt;The chapter also covers in-place operations and the version counter that protects your gradients, &lt;code&gt;train()&lt;/code&gt; versus &lt;code&gt;eval()&lt;/code&gt;, what &lt;code&gt;state_dict&lt;/code&gt; really saves, and a &lt;code&gt;DataLoader&lt;/code&gt; miniature whose first test failure revealed a detail of how PyTorch seeds its workers: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/F-pytorch-in-miniature.md&quot;&gt;chapter F&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;g-sentence-transformers-when-the-miniature-disagrees&quot;&gt;G. sentence-transformers: when the miniature disagrees&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;model.encode()&lt;/code&gt; is four steps: a WordPiece tokenizer, a BERT encoder, a masked average of the token vectors, and a normalization to length 1. The chapter rebuilds all four down to plain Python and NumPy, with the real vocabulary and the real weights of &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; (a 90.9 MB model), and the vectors match &lt;code&gt;model.encode&lt;/code&gt; within 2.6e-07. Along the way it shows two silent behaviors. The first is padding. In a batch, short texts are padded to the length of the longest one, and the transformer computes vectors for the padding positions too. With the miniature's functions, average them in by accident and see what happens to &quot;short&quot; when it shares a batch with a long sentence:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ids, mask = tok.batch([&quot;short&quot;, &quot;a much longer sentence that forces a lot of padding onto the first one&quot;])
tokens = bert(W, ids, mask)
good = normalize(mean_pool(tokens, mask))
naive = normalize(tokens.mean(axis=1))
alone = model.encode([&quot;short&quot;])[0]
print(float(good[0] @ alone), float(naive[0] @ alone))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1.0 0.6889
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the masked mean, &quot;short&quot; gets the same vector it gets alone. With the plain mean, its similarity &lt;em&gt;to itself&lt;/em&gt; is 0.69, and the vector of a text now depends on which other texts happened to be in its batch. The second is that the 256-token limit is a wall:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer(&quot;sentence-transformers/all-MiniLM-L6-v2&quot;)
text = &quot; &quot;.join([&quot;The experiment was a success.&quot;] * 50)        # 302 tokens with [CLS] and [SEP]
a = model.encode([text + &quot; Actually, it failed completely.&quot;])
b = model.encode([text])
print(np.array_equal(a, b))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The conclusion changed completely, and the embedding didn't change at all, because everything after token 256 is cut off before the transformer sees it, without a warning.&lt;/p&gt;
&lt;p&gt;Then the best part. I sent every Unicode code point from U+0020 to U+2FFFF through my tokenizer and through the one sentence-transformers actually runs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;48575 of 194528 code points disagree
 47750  unassigned in Unicode 15.1.0
   419  other (category Mn)
   256  CJK U+2B820 to U+2B91F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(plus 150 more, spread over smaller categories). My rules are the ones in Google's original BERT tokenizer. The library behind sentence-transformers today is Hugging Face's Rust &lt;code&gt;tokenizers&lt;/code&gt;, and its source (version 0.23.3) explains all of it: a Unicode table from 2016, a function documented as covering unassigned characters that doesn't check them, and a typo, &lt;code&gt;0x2B920&lt;/code&gt; where Google's code has &lt;code&gt;0x2B820&lt;/code&gt;. For real text it barely matters, and all 2,000 realistic test texts matched exactly. But &quot;the same tokenizer&quot; turns out to mean &quot;the same implementation&quot;, and if you ever train with one and serve with another, test them on your own text.&lt;/p&gt;
&lt;p&gt;Full chapter, BERT in about forty lines of NumPy included: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/G-sentence-transformers-in-miniature.md&quot;&gt;chapter G&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;h-from-copying-to-writing&quot;&gt;H. From copying to writing&lt;/h3&gt;
&lt;p&gt;The miniatures give you the model. The addendum's chapter H is about using it on problems no tutorial covered, with six habits that cost nothing: predict before you run, rewrite yesterday's code from memory, translate between libraries, break things on purpose, read the source of one function a week, and write a three-line test for anything you believe about a library. It also suggests an order to learn things in, six projects that each have a &quot;done when&quot; (all of them run on a laptop without a GPU), a setup for a modest Windows machine (WSL2 and the CPU-only PyTorch build, which saved me about 3 GB of NVIDIA libraries that would never run), and prompts that keep an assistant in the tutor's chair: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/blob/addendum1/learning-ml-libraries-by-rebuilding-them/chapters/H-from-copying-to-writing.md&quot;&gt;chapter H&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;i-getting-the-addendum&quot;&gt;I. Getting the addendum&lt;/h3&gt;
&lt;p&gt;Everything is in the branch &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts/tree/addendum1/learning-ml-libraries-by-rebuilding-them&quot;&gt;&lt;code&gt;addendum1/learning-ml-libraries-by-rebuilding-them&lt;/code&gt;&lt;/a&gt; of the course repository: eleven chapters (the introduction, the method, the five libraries, the habits, an FAQ with a cheat sheet that has every command on one page, a few final words and twenty references), the miniatures with their tests, and the exact versions. The chapters stand on their own, so if &lt;code&gt;fit_transform&lt;/code&gt; is what bothers you, start at chapter E. To run every check yourself, on a CPU, in a few minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone --branch addendum1/learning-ml-libraries-by-rebuilding-them https://github.com/BlueRingsLabs/no-shortcuts
cd no-shortcuts
pip install torch==2.15.0 --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
cd code &amp;amp;&amp;amp; python test_mini_numpy.py
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;805 checks against NumPy 2.5.4: all passed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the same for the other four (&lt;code&gt;test_mini_pandas.py&lt;/code&gt;, &lt;code&gt;sk/test_mini_sklearn.py&lt;/code&gt;, &lt;code&gt;torch_ch/test_mini_torch.py&lt;/code&gt;, &lt;code&gt;st/test_mini_st.py&lt;/code&gt;). GitHub's CI runs all of them on every push to the branch.&lt;/p&gt;
&lt;h3 id=&quot;j-faq&quot;&gt;J. FAQ&lt;/h3&gt;
&lt;div class=&quot;div-faq&quot;&gt;
&lt;p&gt;&lt;strong&gt;Should I start with the docs, a tutorial, a video or a project?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One short tutorial, to get something running and see the library's shape. Then the explanation pages for the data model and the conventions. Then a miniature, and then a small project without a tutorial. Videos are fine for motivation, but they're the most passive of all of these, and the copy-paste phase feeds on passive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is it normal to look up syntax all the time?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Completely normal, even after years. Memorize the data model and the conventions of each library you use, and let the everyday functions stick by themselves through use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which libraries should I learn, and in what order?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NumPy, pandas, enough matplotlib to look at your data, scikit-learn, PyTorch, then Hugging Face and the RAG tools. The order matters more than the speed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Isn't using an AI assistant just the modern way of learning?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's the modern way of &lt;em&gt;working&lt;/em&gt;, and you should get good at it. While you're learning a library, though, the evidence points the same way as common sense: when the assistant produces the code, you learn less. Use it to quiz you and to explain, and keep the writing for yourself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if my miniature disagrees with the real library?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's a good outcome. Either you've misunderstood something, and now you know exactly what, or you've found a corner of the library that surprised you, which is worth knowing before it surprises you in production. Section G above is the extreme case.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do I need the No Shortcuts course to use this?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No. The addendum stands on its own. The course is where these libraries get used for real, and the end of each chapter points to the relevant lessons.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;k-final-thoughts&quot;&gt;K. Final thoughts&lt;/h3&gt;
&lt;p&gt;If something in here, or in the addendum, is wrong, and in something this long something probably is, tell me in the comments or open an issue on the repository. I'd much rather fix it than leave it. And if this helped you, the best thanks is sharing it with someone who's stuck copy-pasting. Thanks for reading :B&lt;/p&gt;
&lt;h3 id=&quot;l-references&quot;&gt;L. References&lt;/h3&gt;
&lt;p&gt;Listed in order of first appearance.&lt;/p&gt;
&lt;p&gt;\[1\] J. H. Shen and A. Tamkin, &quot;How AI impacts skill formation,&quot; arXiv:2601.20245, 2026. &lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/2601.20245&quot;&gt;https://arxiv.org/abs/2601.20245&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[2\] N. J. Slamecka and P. Graf, &quot;The generation effect: Delineation of a phenomenon,&quot; &lt;em&gt;Journal of Experimental Psychology: Human Learning and Memory&lt;/em&gt;, vol. 4, no. 6, pp. 592–604, 1978. &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1037/0278-7393.4.6.592&quot;&gt;https://doi.org/10.1037/0278-7393.4.6.592&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[3\] S. Bertsch, B. J. Pesta, R. Wiscott and M. A. McDaniel, &quot;The generation effect: A meta-analytic review,&quot; &lt;em&gt;Memory &amp;amp; Cognition&lt;/em&gt;, vol. 35, no. 2, pp. 201–210, 2007. &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.3758/BF03193441&quot;&gt;https://doi.org/10.3758/BF03193441&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[4\] K. Morehead, J. Dunlosky and K. A. Rawson, &quot;How much mightier is the pen than the keyboard for note-taking? A replication and extension of Mueller and Oppenheimer (2014),&quot; &lt;em&gt;Educational Psychology Review&lt;/em&gt;, vol. 31, no. 3, pp. 753–780, 2019. &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1007/s10648-019-09468-2&quot;&gt;https://doi.org/10.1007/s10648-019-09468-2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[5\] G. L. Romero, &quot;No Shortcuts: Data, Machine Learning, Deep Learning and AI Engineering, the long way round,&quot; GitHub, 2026. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BlueRingsLabs/no-shortcuts&quot;&gt;https://github.com/BlueRingsLabs/no-shortcuts&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[6\] D. Procida, &quot;Diátaxis: a systematic approach to technical documentation authoring.&quot; &lt;a rel=&quot;nofollow&quot; href=&quot;https://diataxis.fr/&quot;&gt;https://diataxis.fr/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[7\] T. Hastie, R. Tibshirani and J. Friedman, &lt;em&gt;The Elements of Statistical Learning&lt;/em&gt;, 2nd ed., Springer, 2009, sec. 7.10.2. &lt;a rel=&quot;nofollow&quot; href=&quot;https://hastie.su.domains/ElemStatLearn/&quot;&gt;https://hastie.su.domains/ElemStatLearn/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;\[8\] S. Kapoor and A. Narayanan, &quot;Leakage and the reproducibility crisis in machine-learning-based science,&quot; &lt;em&gt;Patterns&lt;/em&gt;, vol. 4, no. 9, art. 100804, 2023. &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1016/j.patter.2023.100804&quot;&gt;https://doi.org/10.1016/j.patter.2023.100804&lt;/a&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/32903/learn-any-library-rebuilding-numpy-pandas-scikit-learn-pytorch-and-sentence-transformer</guid>
<pubDate>Wed, 28 Oct 2026 23:45:11 +0000</pubDate>
</item>
<item>
<title>How Python Can Make Your Daily Life Easier</title>
<link>https://coderlegion.com/32857/how-python-can-make-your-daily-life-easier</link>
<description>&lt;p&gt;Most of us spend a surprising amount of time doing small, repetitive tasks. Renaming files, organizing folders, updating spreadsheets, checking data, writing reports, and copying information from one place to another can easily take hours every week.&lt;/p&gt;
&lt;p&gt;The problem is that many of these tasks don't require much thinking. They simply require time.&lt;/p&gt;
&lt;p&gt;This is where Python becomes useful.&lt;/p&gt;
&lt;p&gt;Python isn't just a programming language for building websites, AI tools, or complex applications. It can also be a practical tool for simplifying everyday work, reducing repetitive tasks, and freeing up time for things that actually need your attention.&lt;/p&gt;
&lt;h3 id=&quot;1-automate-repetitive-tasks&quot;&gt;1. Automate Repetitive Tasks&lt;/h3&gt;
&lt;p&gt;Think about the tasks you repeat almost every day. Maybe you download files, rename documents, organize folders, or move information between different applications.&lt;/p&gt;
&lt;p&gt;Doing these things manually isn't difficult, but it gets boring when you have to repeat the same process hundreds of times.&lt;/p&gt;
&lt;p&gt;Python can automate many of these activities.&lt;/p&gt;
&lt;p&gt;For example, a simple script can rename hundreds of files, move documents into appropriate folders, or create organized directories for different projects. Python's built-in &lt;code&gt;pathlib&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, and &lt;code&gt;shutil&lt;/code&gt; modules provide tools for working with files and folders.&lt;/p&gt;
&lt;p&gt;Instead of spending 30 minutes organizing files every day, you can write a script once and reuse it whenever necessary.&lt;/p&gt;
&lt;p&gt;That's one of the biggest benefits of automation: &lt;strong&gt;solve the problem once, then let the code handle the repetition.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-work-smarter-with-excel-and-csv-files&quot;&gt;2. Work Smarter With Excel and CSV Files&lt;/h3&gt;
&lt;p&gt;Spreadsheets are part of everyday work for freelancers, business owners, marketers, and developers.&lt;/p&gt;
&lt;p&gt;Imagine receiving five Excel or CSV files containing thousands of records. You need to combine them, remove duplicates, clean up missing values, and prepare a final report.&lt;/p&gt;
&lt;p&gt;Doing everything manually could take hours.&lt;/p&gt;
&lt;p&gt;Python can handle much of this work automatically. Libraries such as &lt;code&gt;pandas&lt;/code&gt; and &lt;code&gt;openpyxl&lt;/code&gt; help with data processing and Excel files, while Python's built-in &lt;code&gt;csv&lt;/code&gt; module can read and write CSV data.&lt;/p&gt;
&lt;p&gt;You could build a script that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Combines multiple CSV files into one.&lt;/li&gt;
&lt;li&gt;Finds and removes duplicate records.&lt;/li&gt;
&lt;li&gt;Standardizes phone numbers and email addresses.&lt;/li&gt;
&lt;li&gt;Filters data based on specific conditions.&lt;/li&gt;
&lt;li&gt;Generates a report from the cleaned information.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For someone who works with data regularly, these small automations can make a significant difference.&lt;/p&gt;
&lt;h3 id=&quot;3-save-time-on-everyday-business-tasks&quot;&gt;3. Save Time on Everyday Business Tasks&lt;/h3&gt;
&lt;p&gt;Running a small business involves much more than selling products or providing services. There are invoices to prepare, expenses to record, stock levels to monitor, and reports to review.&lt;/p&gt;
&lt;p&gt;Python can help bring these tasks into one workflow.&lt;/p&gt;
&lt;p&gt;For example, a small shop could use a Python application to record sales, update inventory, calculate daily revenue, and generate monthly reports.&lt;/p&gt;
&lt;p&gt;A freelancer could build a tool to organize client information, track project deadlines, prepare invoices, and summarize earnings.&lt;/p&gt;
&lt;p&gt;These tools don't have to be complicated. Even a small desktop application with a simple interface can solve a real problem.&lt;/p&gt;
&lt;p&gt;The goal isn't to automate everything. It's to identify the tasks that consume too much time and make them easier to manage.&lt;/p&gt;
&lt;h3 id=&quot;4-organize-your-digital-workspace&quot;&gt;4. Organize Your Digital Workspace&lt;/h3&gt;
&lt;p&gt;A cluttered computer can slow you down more than you might expect.&lt;/p&gt;
&lt;p&gt;Downloads folders often become filled with screenshots, PDFs, images, documents, and installation files. Finding a particular file becomes harder as the collection grows.&lt;/p&gt;
&lt;p&gt;With Python, you can create a folder organizer that sorts files by their extensions or other rules you define.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PDF files go into a Documents folder.&lt;/li&gt;
&lt;li&gt;Images go into an Images folder.&lt;/li&gt;
&lt;li&gt;Spreadsheet files go into a Reports folder.&lt;/li&gt;
&lt;li&gt;Compressed files go into an Archives folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also create scripts to back up important project files or generate dated copies of reports.&lt;/p&gt;
&lt;p&gt;Just make sure to test file-moving and deletion scripts on sample folders first, so automation doesn't accidentally overwrite or remove important data.&lt;/p&gt;
&lt;h3 id=&quot;5-reduce-the-time-spent-on-web-research&quot;&gt;5. Reduce the Time Spent on Web Research&lt;/h3&gt;
&lt;p&gt;Many jobs involve collecting information from websites, checking public data, or comparing information across multiple sources.&lt;/p&gt;
&lt;p&gt;Python can help automate parts of this process through APIs and, where permitted, web scraping.&lt;/p&gt;
&lt;p&gt;For example, you might build a script that collects information from an authorized public API, checks whether records are missing important fields, and saves the results to a spreadsheet.&lt;/p&gt;
&lt;p&gt;You could also create a research tool that organizes links, categorizes collected information, or produces a summary of your findings.&lt;/p&gt;
&lt;p&gt;Of course, automation should respect website terms, privacy requirements, rate limits, and access restrictions. It is also important to validate the information rather than assuming every result is accurate.&lt;/p&gt;
&lt;p&gt;The real advantage is that Python can turn a repetitive research process into a consistent workflow.&lt;/p&gt;
&lt;h3 id=&quot;6-create-your-own-personal-tools&quot;&gt;6. Create Your Own Personal Tools&lt;/h3&gt;
&lt;p&gt;One of my favorite things about Python is that you don't always need to search for an existing application to solve a problem.&lt;/p&gt;
&lt;p&gt;Sometimes, building a small tool yourself is the better option.&lt;/p&gt;
&lt;p&gt;You could create:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A personal expense tracker.&lt;/li&gt;
&lt;li&gt;A daily task reminder.&lt;/li&gt;
&lt;li&gt;A file backup utility.&lt;/li&gt;
&lt;li&gt;A password-protected notes application.&lt;/li&gt;
&lt;li&gt;A YouTube video information tracker.&lt;/li&gt;
&lt;li&gt;A job-search organizer.&lt;/li&gt;
&lt;li&gt;A simple desktop application for managing projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using libraries such as Tkinter or CustomTkinter, you can build desktop applications with buttons, forms, tables, and search features.&lt;/p&gt;
&lt;p&gt;You don't need to build the next big software product. A small tool that saves you ten minutes every day can already be worthwhile.&lt;/p&gt;
&lt;h3 id=&quot;7-let-python-handle-reports-and-routine-checks&quot;&gt;7. Let Python Handle Reports and Routine Checks&lt;/h3&gt;
&lt;p&gt;Reporting is another area where automation can save time.&lt;/p&gt;
&lt;p&gt;Suppose you need to prepare a report every Friday. You collect information from spreadsheets, calculate totals, compare results, and format everything into a document.&lt;/p&gt;
&lt;p&gt;Python can automate much of this workflow.&lt;/p&gt;
&lt;p&gt;A script could read the relevant files, calculate totals, identify unusual values, and generate a report in a consistent format.&lt;/p&gt;
&lt;p&gt;You can also schedule scripts to run at specific times, depending on your operating system and setup.&lt;/p&gt;
&lt;p&gt;However, automated reports still need sensible checks. If the input data is incomplete or incorrect, the output can be misleading too.&lt;/p&gt;
&lt;p&gt;Automation makes the process faster, but good data and careful validation remain essential.&lt;/p&gt;
&lt;h3 id=&quot;8-start-small-instead-of-automating-everything&quot;&gt;8. Start Small Instead of Automating Everything&lt;/h3&gt;
&lt;p&gt;A common mistake when learning Python is starting with a huge project before understanding the actual problem.&lt;/p&gt;
&lt;p&gt;You don't need to build a complete business management system on your first attempt.&lt;/p&gt;
&lt;p&gt;Start by asking yourself three simple questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which task do I repeat most often?&lt;/li&gt;
&lt;li&gt;How much time does that task take each week?&lt;/li&gt;
&lt;li&gt;Could a small Python script handle part of it safely?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Choose one task and build a basic solution.&lt;/p&gt;
&lt;p&gt;If you regularly rename files, automate file renaming. If you spend hours cleaning spreadsheets, start with a CSV-cleaning script. If you manually calculate expenses, build a simple expense calculator.&lt;/p&gt;
&lt;p&gt;Once the first version works reliably, you can improve it with a graphical interface, error handling, logging, and additional features.&lt;/p&gt;
&lt;p&gt;The best automation projects often begin with a very ordinary problem.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Python won't eliminate every repetitive task, and not every task is worth automating. Sometimes, writing and maintaining a script takes longer than doing the task manually.&lt;/p&gt;
&lt;p&gt;But when the same process happens again and again, Python can become a valuable assistant.&lt;/p&gt;
&lt;p&gt;It can organize files, process data, prepare reports, simplify business workflows, and help you build tools that fit your specific needs.&lt;/p&gt;
&lt;p&gt;You don't have to be an expert to get started. You just need to notice the little things that waste your time and think about how code could make them easier.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The real value of Python isn't just in what you can build. It's in the time and effort you can save by building the right thing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What is one repetitive task in your daily life that you would love to automate with Python?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32857/how-python-can-make-your-daily-life-easier</guid>
<pubDate>Wed, 28 Oct 2026 14:49:13 +0000</pubDate>
</item>
<item>
<title>I've been gone for 5 months. Here is what I built while I was away (and how you can help me test i</title>
<link>https://coderlegion.com/32836/ive-been-gone-for-5-months-here-is-what-i-built-while-i-was-away-and-how-you-can-help-me-test-i</link>
<description>&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;
Hey CoderLegion! 👋

It’s been a minute since I last posted here. I went heads-down into the trenches of building, and to be honest, I neglected this community while focusing heavily on LinkedIn.

But I’m back, and I brought something with me.

When I first shared Permi here, it was a basic CLI security scanner. Since then, based on feedback from data-protection lawyers, CISOs, and senior engineers, it has evolved massively.

## What Permi does now:

- **Nigerian-Specific Scanning:** Catches hardcoded BVNs, exposed Paystack/Flutterwave keys, USSD flaws, and PII in logs that global tools miss.
- **AI Noise Reduction:** Cuts out ~95% of false positives so you only see what's real.
- **Automated Compliance:** It maps findings to NDPA/CBN controls and generates an automated Excel Readiness Workbook (yes, it calculates the final compliance score and tracks evidence provenance).

## Why I’m posting:

I am moving into a critical beta phase. I don't just want downloads; I want technical feedback. I need developers and security folks to break it, test it, and tell me what’s missing.

## How you can help (Zero pressure):

### Option 1: Run it yourself

You can install it in a Python environment and run it on a staging/demo repo:

```bash
pip install permi
permi setup --api-key YOUR_API_KEY_HERE
permi scan ./your-codebase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The API key is free from openrouter.ai for a few test calls).&lt;/p&gt;
&lt;h4 id=&quot;option-2-let-me-run-it-for-you&quot;&gt;Option 2: Let me run it for you&lt;/h4&gt;
&lt;p&gt;If you don't feel like setting it up, just drop a link to a public GitHub repo in the comments or DM me. I'll run the scan on my end and send you the PDF report + the automated Excel workbook.&lt;/p&gt;
&lt;p&gt;If you test it, please reply here or DM me. Tell me: Did it catch anything real? Was the setup smooth? What frustrated you?&lt;/p&gt;
&lt;p&gt;Let's build something useful for the Nigerian/African ecosystem together. 🚀&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hashtags:&lt;/strong&gt; #Python #CyberSecurity #AppSec #DevTools #OpenSource #NDPA #NigerianTech&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32836/ive-been-gone-for-5-months-here-is-what-i-built-while-i-was-away-and-how-you-can-help-me-test-i</guid>
<pubDate>Wed, 28 Oct 2026 12:25:11 +0000</pubDate>
</item>
<item>
<title>Building DocuMind AI: From PDF Text and OCR to Open-Weight LLMs and Human-Verified Expenses</title>
<link>https://coderlegion.com/32793/building-documind-ai-from-pdf-text-and-ocr-to-open-weight-llms-and-human-verified-expenses</link>
<description>&lt;p&gt;&lt;strong&gt;By Imran Ghafoor&lt;/strong&gt; · Python · Flask · React · PyMuPDF · Tesseract · Groq · GPT-OSS · Docker · Render&lt;/p&gt;
&lt;p&gt;A receipt looks simple until you try to turn it into reliable software data. The vendor may be printed at the top, the date may use an unfamiliar format, and the largest number on the page is not necessarily the amount paid. Sometimes there is no text layer at all—just a photograph embedded in a PDF.&lt;/p&gt;
&lt;p&gt;I built &lt;strong&gt;DocuMind AI&lt;/strong&gt; to explore a practical answer: &lt;strong&gt;combine conventional document extraction with an open-weight language model, validate its output, and let a person approve the final record.&lt;/strong&gt; It began as a Hacktoberfest Side Hustle project and became a useful exercise in building an end-to-end AI application under real hosting constraints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://documind-demo-web.onrender.com/#/&quot;&gt;Try the live demo&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594/documind-ai&quot;&gt;Explore the GitHub repository&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Demo Access Code: Demo_documind@678!&lt;/p&gt;
&lt;p&gt;The live version is a &lt;strong&gt;shared, temporary demo&lt;/strong&gt;. Use only fictional invoices and receipts; the demo access code is available in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/meimranghafoor/documind-ai-turning-receipts-into-business-insights-with-open-weight-ai-3982&quot;&gt;original DEV Community project post&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-recognizing-text-is-not-understanding-a-document&quot;&gt;The Problem: Recognizing Text Is Not Understanding a Document&lt;/h3&gt;
&lt;p&gt;Imagine a freelancer with PDF invoices, phone photos of receipts, and scanned bills. Each document contains information that should eventually become a consistent expense record: vendor, date, currency, subtotal, tax, total, and category.&lt;/p&gt;
&lt;p&gt;OCR can recognize words, but it does not necessarily decide which amount is the &lt;strong&gt;final payable total&lt;/strong&gt; or whether an ambiguous date means March 4 or April 3. An LLM can help interpret context, but it introduces its own failure modes: missing values, invented fields, inconsistent JSON, and amounts that look plausible but are wrong.&lt;/p&gt;
&lt;p&gt;That led to the guiding rule for DocuMind AI:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;AI output is a proposal, not a confirmed financial fact.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The application therefore separates document ingestion, text extraction, semantic interpretation, validation, and human confirmation. A successful API response alone does not make a document an expense.&lt;/p&gt;
&lt;h3 id=&quot;how-the-system-fits-together&quot;&gt;How the System Fits Together&lt;/h3&gt;
&lt;p&gt;The frontend is built with &lt;strong&gt;React, TypeScript, Vite, and Tailwind CSS&lt;/strong&gt;. &lt;strong&gt;Flask&lt;/strong&gt; handles document uploads and API requests, while &lt;strong&gt;SQLAlchemy&lt;/strong&gt; manages application records. The processing pipeline sits behind the API so the browser never needs to run OCR or hold the AI provider's secret key.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;           PDF / PNG / JPG upload
                     |
                     v
              Flask validation
                     |
               +-----+------+
               |            |
               v            v
          PyMuPDF       Tesseract OCR
       (selectable text)  (scan/image)
               |            |
               +-----+------+
                     |
                 Plain text
                     |
                     v
            Groq + GPT-OSS 20B
                     |
               Suggested JSON
                     |
              Python validation
                     |
              Human review queue
                     |
           Confirmed expense record
                     |
            Dashboard / CSV reports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation makes debugging more useful. If a total is wrong, I can investigate whether the text was recognized incorrectly, the model interpreted the text incorrectly, or the final value needs a user correction.&lt;/p&gt;
&lt;h3 id=&quot;engineering-decision-1-read-native-pdfs-before-running-ocr&quot;&gt;Engineering Decision 1: Read Native PDFs Before Running OCR&lt;/h3&gt;
&lt;p&gt;One implementation choice I wanted to preserve was &lt;strong&gt;not running OCR on every PDF&lt;/strong&gt;. Digital PDFs often contain selectable text. Rendering those pages into images and running OCR would add work without necessarily improving the result.&lt;/p&gt;
&lt;p&gt;The backend uses &lt;a rel=&quot;nofollow&quot; href=&quot;https://pymupdf.readthedocs.io/en/latest/recipes-text.html&quot;&gt;PyMuPDF&lt;/a&gt; first, then checks whether enough native text was extracted. Here is a shortened excerpt of the actual decision logic in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594/documind-ai/blob/main/backend/app/services/extraction.py&quot;&gt;&lt;code&gt;backend/app/services/extraction.py&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;native = &quot;\n&quot;.join(
    doc[i].get_text(&quot;text&quot;) for i in range(pages)
).strip()

if len(re.sub(r&quot;\s+&quot;, &quot;&quot;, native)) &amp;gt;= settings.min_pdf_text_chars:
    return ExtractionResult(native, &quot;pdf_text&quot;, pages, warnings)

# Otherwise, fall back to the OCR path.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application's default threshold is &lt;strong&gt;40 non-whitespace characters&lt;/strong&gt;. That is a heuristic, not a perfect scanned-PDF detector. A nearly empty digital document may also fall back to OCR, and a document with a little selectable text may still have important information embedded as images.&lt;/p&gt;
&lt;p&gt;For a scanned PDF, the backend renders pages using PyMuPDF and passes the resulting images to &lt;strong&gt;Tesseract&lt;/strong&gt;. Direct PNG/JPG uploads go to Tesseract as well. The server imposes page, image-pixel, and timeout limits so one large document does not consume unlimited resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; The hybrid approach handles common digital and scanned files without assuming that either method is always sufficient. It still struggles with very poor scans, unusual layouts, and handwriting. I have not measured extraction accuracy on a representative benchmark dataset.&lt;/p&gt;
&lt;h3 id=&quot;engineering-decision-2-make-the-llm-return-data-not-a-paragraph&quot;&gt;Engineering Decision 2: Make the LLM Return Data, Not a Paragraph&lt;/h3&gt;
&lt;p&gt;Once the text has been extracted, the next task is &lt;em&gt;interpretation&lt;/em&gt;. The deployed demo is configured for &lt;strong&gt;&lt;code&gt;openai/gpt-oss-20b&lt;/code&gt;&lt;/strong&gt;, an open-weight model accessed through &lt;strong&gt;Groq's hosted inference API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I did not run the language model inside the Render container. Hosted inference kept the web service focused on document processing and application logic, which mattered for a resource-limited demo.&lt;/p&gt;
&lt;p&gt;The backend requests a JSON object containing document type, vendor, date, currency, subtotal, tax, total amount, expense category, and a short summary. A fictional receipt might produce a proposal like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;document_type&quot;: &quot;receipt&quot;,
  &quot;vendor&quot;: &quot;ABC Electronics &amp;amp; Stationery&quot;,
  &quot;document_date&quot;: &quot;2026-10-18&quot;,
  &quot;currency&quot;: &quot;PKR&quot;,
  &quot;subtotal&quot;: &quot;5500.00&quot;,
  &quot;tax&quot;: &quot;500.00&quot;,
  &quot;total_amount&quot;: &quot;6000.00&quot;,
  &quot;expense_category&quot;: &quot;Office Supplies&quot;,
  &quot;short_summary&quot;: &quot;Stationery and equipment purchase&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;This JSON is an illustrative target structure using synthetic data, not a published model-accuracy measurement.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Rather than hard-coding one SDK throughout the project, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594/documind-ai/blob/main/backend/app/services/ai.py&quot;&gt;&lt;code&gt;backend/app/services/ai.py&lt;/code&gt;&lt;/a&gt; communicates through an OpenAI-compatible chat-completions interface. The request includes the configured model, a system instruction, document text, and JSON response mode when supported:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;body = {
    &quot;model&quot;: self.s.ai_model,
    &quot;temperature&quot;: 0,
    &quot;max_completion_tokens&quot;: self.s.ai_max_completion_tokens,
    &quot;messages&quot;: [
        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system},
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user},
    ],
}

if self.s.ai_json_mode:
    body[&quot;response_format&quot;] = {&quot;type&quot;: &quot;json_object&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prompt explicitly asks the model &lt;strong&gt;not to invent missing values&lt;/strong&gt;, to return &lt;code&gt;null&lt;/code&gt; when uncertain, and to treat the uploaded document as &lt;strong&gt;untrusted text&lt;/strong&gt;. The client also distinguishes authentication problems, rate limits, timeouts, and invalid output. &lt;a rel=&quot;nofollow&quot; href=&quot;https://console.groq.com/docs/structured-outputs&quot;&gt;Groq's structured-output documentation&lt;/a&gt; describes the available JSON-oriented response options and their different guarantees.&lt;/p&gt;
&lt;p&gt;There is an important boundary here: &lt;strong&gt;asking for JSON does not make the values correct&lt;/strong&gt;. That is why the next stage does not simply insert the model response into financial reports.&lt;/p&gt;
&lt;h3 id=&quot;engineering-decision-3-validate-and-review-before-confirming&quot;&gt;Engineering Decision 3: Validate and Review Before Confirming&lt;/h3&gt;
&lt;p&gt;DocuMind AI validates the proposed fields on the server. Dates must be real calendar dates, currencies must use three-letter codes, categories must match supported values, and monetary amounts must be non-negative, finite, and appropriately normalized.&lt;/p&gt;
&lt;p&gt;The validation code deliberately treats model suggestions differently from human edits. This excerpt is taken from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594/documind-ai/blob/main/backend/app/validation.py&quot;&gt;&lt;code&gt;backend/app/validation.py&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def validate_fields(raw: dict, *, lenient: bool) -&amp;gt; ValidationResult:
    &quot;&quot;&quot;lenient=True: invalid AI values become null + warnings.
    lenient=False: invalid user values become errors.&quot;&quot;&quot;
    result = ValidationResult()
    for name in FIELD_NAMES:
        value = raw.get(name)
        if _blank(value):
            result.fields[name] = None
            continue
        try:
            result.fields[name] = _PARSERS[name](value)
        except FieldError as exc:
            result.fields[name] = None
            if lenient:
                result.warnings.append(
                    f&quot;{name}: {exc} (value discarded, please check the document)&quot;
                )
            else:
                result.errors[name] = str(exc)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A model response such as &lt;code&gt;&quot;document_date&quot;: &quot;2026-15-45&quot;&lt;/code&gt; should become a warning, not a believable date. A human editing a record should receive an actionable validation error instead of silently losing their input.&lt;/p&gt;
&lt;p&gt;The application also performs &lt;em&gt;soft consistency checks&lt;/em&gt;. If &lt;code&gt;subtotal + tax&lt;/code&gt; differs from the total, it warns the reviewer. It does not automatically declare the document invalid because legitimate invoices can include shipping, discounts, or rounding.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Review Queue&lt;/strong&gt; is where these safeguards become visible. The interface lets users inspect an original document alongside suggested values, correct fields, and confirm a record explicitly. It also records whether a value came from AI, a template, or a person.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Imran-Ghafoor594/documind-ai/main/docs/screenshots/ai-extraction.png&quot; alt=&quot;DocuMind AI: AI extraction workspace&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 1. The extraction workspace presents model-suggested fields for inspection.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Imran-Ghafoor594/documind-ai/main/docs/screenshots/review-queue.png&quot; alt=&quot;DocuMind AI: Review queue&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 2. The review workflow separates a processed document from a confirmed expense.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Only confirmed records contribute to expense totals.&lt;/strong&gt; That design choice matters more to me than making the application appear fully automatic.&lt;/p&gt;
&lt;h3 id=&quot;three-real-challenges-behind-the-live-demo&quot;&gt;Three Real Challenges Behind the Live Demo&lt;/h3&gt;
&lt;h4 id=&quot;1-ocr-belonged-on-the-server-not-on-my-laptop&quot;&gt;1. OCR belonged on the server, not on my laptop&lt;/h4&gt;
&lt;p&gt;Early in deployment planning, I had to separate &lt;strong&gt;local development dependencies&lt;/strong&gt; from &lt;strong&gt;runtime dependencies&lt;/strong&gt;. If users access DocuMind AI through a website, they should not need to install OCR software on their machines.&lt;/p&gt;
&lt;p&gt;The solution was to package Tesseract inside the backend's Docker image. The relevant lines from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594/documind-ai/blob/main/backend/Dockerfile&quot;&gt;&lt;code&gt;backend/Dockerfile&lt;/code&gt;&lt;/a&gt; are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM python:3.12-slim-bookworm

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    tesseract-ocr tesseract-ocr-eng \
    &amp;amp;&amp;amp; tesseract --version \
    &amp;amp;&amp;amp; tesseract --list-langs \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That made the OCR runtime part of the deployment artifact, rather than an assumption about the hosting machine. For details on Tesseract's capabilities and language data, see the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tesseract-ocr.github.io/tessdoc/&quot;&gt;official Tesseract documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;2-the-first-deployment-plan-was-unnecessarily-expensive-for-a-prototype&quot;&gt;2. The first deployment plan was unnecessarily expensive for a prototype&lt;/h4&gt;
&lt;p&gt;An initial infrastructure configuration estimated approximately &lt;strong&gt;$31.25 per month&lt;/strong&gt;, covering a larger backend instance, database, and persistent upload storage. Those resources can be appropriate for a long-lived application, but I only needed a temporary hackathon demonstration.&lt;/p&gt;
&lt;p&gt;I moved to a separate &lt;strong&gt;&lt;code&gt;render-free.yaml&lt;/code&gt;&lt;/strong&gt; Blueprint and kept the paid configuration separate. The free-demo configuration limits files to &lt;strong&gt;3 MB&lt;/strong&gt;, processes up to &lt;strong&gt;3 PDF pages&lt;/strong&gt;, reduces OCR rendering to &lt;strong&gt;100 DPI&lt;/strong&gt;, and stores the SQLite database and uploads in temporary storage.&lt;/p&gt;
&lt;p&gt;The trade-off is significant: Render's free web services may spin down, and local files—including a SQLite database—can be lost after a spin-down, restart, or redeployment. The live demo is therefore &lt;strong&gt;not suitable for real financial records or permanent storage&lt;/strong&gt;. &lt;a rel=&quot;nofollow&quot; href=&quot;https://render.com/docs/free&quot;&gt;Render documents these free-tier limitations&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This experience reinforced a practical lesson: &lt;strong&gt;optimize infrastructure for the purpose of the deployment, not for the imagined future version of the product.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;3-api-configuration-was-a-deployment-problem-of-its-own&quot;&gt;3. API configuration was a deployment problem of its own&lt;/h4&gt;
&lt;p&gt;While setting up GroqCloud, I encountered an API-key creation error: &lt;strong&gt;“We couldn't verify this request.”&lt;/strong&gt; It was a useful reminder that an AI application can fail before model inference even begins—account verification, credentials, environment variables, and provider availability all need attention.&lt;/p&gt;
&lt;p&gt;After resolving the key setup and deploying the application, the Flask health endpoint returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;ai_configured&quot;: true,
  &quot;database&quot;: &quot;ok&quot;,
  &quot;ocr_available&quot;: true,
  &quot;status&quot;: &quot;ok&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That response confirmed the application could see its configuration, database, and OCR runtime. It &lt;strong&gt;did not prove that the language model was accurate&lt;/strong&gt;. I separately uploaded synthetic receipts and tested the browser workflow after deployment.&lt;/p&gt;
&lt;h3 id=&quot;what-i-tested-and-what-i-haven-t-claimed&quot;&gt;What I Tested—and What I Haven't Claimed&lt;/h3&gt;
&lt;p&gt;I tested the live demo with fictional receipt and invoice documents, including PDF and image formats, and used the dashboard to inspect extracted information and the review workflow. The project also contains automated backend, frontend, and browser-test suites, which are available in the repository.&lt;/p&gt;
&lt;p&gt;The most important distinction is between &lt;strong&gt;a working workflow&lt;/strong&gt; and &lt;strong&gt;a validated extraction benchmark&lt;/strong&gt;. I have not published field-level precision, recall, error rates, or a statistically meaningful comparison against competing document AI systems. I also have not claimed paying customers.&lt;/p&gt;
&lt;p&gt;A useful next evaluation would build a labeled set of invoices with varied scan quality and layouts, then measure vendor/date/amount extraction separately. Accuracy on a few clean synthetic receipts would not be enough to support production claims.&lt;/p&gt;
&lt;h3 id=&quot;what-would-have-to-change-before-real-customers-could-use-it-&quot;&gt;What Would Have to Change Before Real Customers Could Use It?&lt;/h3&gt;
&lt;p&gt;The current demo uses a &lt;strong&gt;shared access code&lt;/strong&gt; and short-lived signed sessions. That is convenient for reviewers, but it is not multi-user authentication or document-level isolation. Combined with temporary local storage and the fact that document text is sent to a third-party inference provider, it means the demo should be used &lt;strong&gt;only with synthetic data&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Before treating this as a real SaaS product, I would prioritize proper accounts and authorization, persistent database and object storage, background OCR/AI jobs, privacy controls and retention policies, duplicate detection, and systematic model evaluation. In particular, synchronous extraction is simple for a prototype but becomes a bottleneck as document volume and processing time increase.&lt;/p&gt;
&lt;h3 id=&quot;lessons-i-d-carry-into-the-next-ai-project&quot;&gt;Lessons I'd Carry Into the Next AI Project&lt;/h3&gt;
&lt;p&gt;The most useful lesson was not a new prompt technique. It was learning to keep &lt;strong&gt;recognition&lt;/strong&gt;, &lt;strong&gt;interpretation&lt;/strong&gt;, &lt;strong&gt;validation&lt;/strong&gt;, and &lt;strong&gt;approval&lt;/strong&gt; as different responsibilities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use specialized tools first.&lt;/strong&gt; PyMuPDF and Tesseract do work that does not require an LLM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat model output as untrusted input.&lt;/strong&gt; JSON parsing, value validation, and consistency checks belong in application code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design for failures.&lt;/strong&gt; An unavailable AI provider should not cause an uploaded document to disappear or be falsely marked as successfully extracted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make the user's decision visible.&lt;/strong&gt; Confirmation and field provenance make the resulting reports easier to trust.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be explicit about prototype limitations.&lt;/strong&gt; A live URL does not imply reliable storage, multi-user security, or measured accuracy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I used AI development assistance during the prototyping and iteration process, but the architecture, hosted configuration, repository, and live workflow are open to inspection. I would rather describe what the system actually does than present an unmeasured result as a breakthrough.&lt;/p&gt;
&lt;h3 id=&quot;try-the-project-and-explore-the-code&quot;&gt;Try the Project and Explore the Code&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://documind-demo-web.onrender.com/#/&quot;&gt;DocuMind AI on Render&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo Access Code:&lt;/strong&gt; Demo_documind@678!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594/documind-ai&quot;&gt;Imran-Ghafoor594/documind-ai&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Original project showcase:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/meimranghafoor/documind-ai-turning-receipts-into-business-insights-with-open-weight-ai-3982&quot;&gt;DEV Community — DocuMind AI: Turning Receipts into Business Insights&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Imran-Ghafoor594/documind-ai/main/docs/screenshots/expenses.png&quot; alt=&quot;DocuMind AI expense dashboard&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 3. Confirmed records feed the expense view rather than unreviewed AI output.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;DocuMind AI started as a small side-hustle concept: help people spend less time copying invoice fields into spreadsheets. Building it turned into a broader engineering lesson about &lt;strong&gt;how to add AI without surrendering data quality to it&lt;/strong&gt;. The result is still a prototype—but it is an inspectable, deployed prototype with clearly defined boundaries and a path toward a more reliable system.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt; I'm &lt;strong&gt;Imran Ghafoor&lt;/strong&gt;, an AI developer pursuing a Bachelor's degree in Artificial Intelligence. I work on practical projects involving machine learning, computer vision, and LLM-based applications. &lt;br&gt;
Find more of my work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Imran-Ghafoor594&quot;&gt;GitHub&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://imranghafoor.vercel.app/&quot;&gt;my portfolio&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32793/building-documind-ai-from-pdf-text-and-ocr-to-open-weight-llms-and-human-verified-expenses</guid>
<pubDate>Wed, 28 Oct 2026 09:57:51 +0000</pubDate>
</item>
<item>
<title>Stop calling functions: my first LangGraph graph, where state does the calling</title>
<link>https://coderlegion.com/32792/stop-calling-functions-my-first-langgraph-graph-where-state-does-the-calling</link>
<description>&lt;p&gt;Three parts in, JobCopilot had layers that worked but couldn't talk to each other. Part 1 gave me a structured-output layer — &lt;code&gt;ParseOk&lt;/code&gt;/&lt;code&gt;ParseError&lt;/code&gt; as data. Part 2 gave me retrieval — &lt;code&gt;build_context()&lt;/code&gt; pulling the top-8 resume chunks for a job description. The pipeline was one line on paper; in code it was nested calls, and the awkward questions had no home: who retries when the model drifts into prose? Who &lt;em&gt;stops&lt;/em&gt; the retries? Where does the score live between &quot;parsed&quot; and &quot;shown to the user&quot;?&lt;/p&gt;
&lt;p&gt;I did what a React dev does in a new language: I nested functions. The retry loop closed over a closure that closed over a callback, and I couldn't tell you who owned what. Rewriting the flow as a graph — my first with &lt;a rel=&quot;nofollow&quot; href=&quot;https://python.langchain.com/docs/introduction/&quot;&gt;LangGraph&lt;/a&gt; — made the nesting disappear. Not because a framework absorbed it, but because the mental model changed: &lt;strong&gt;functions stop calling each other; state flows through a graph, and nodes return patches of that state.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It survived everything I threw at it — after I fixed one wire that made &lt;em&gt;every&lt;/em&gt; job come back &lt;code&gt;&quot;error&quot;&lt;/code&gt;, no matter how perfectly the model answered.&lt;/p&gt;
&amp;lt;!-- NOTE TO V: the JobCopilot mirror isn't on this machine, so code below is the canonical pattern rebuilt from the part-1/part-2 contracts (ParseOk/ParseError, build_context, TOP_K=8). Before publishing: align file names, node names and prompt wording with your repo. --&amp;gt;
&lt;h3 id=&quot;1-the-state-is-a-pydantic-model-nodes-return-patches&quot;&gt;1. The state is a pydantic model; nodes return patches&lt;/h3&gt;
&lt;p&gt;If the graph is the wiring, state is everything a run knows. I made it one pydantic model — deliberately the &lt;em&gt;same&lt;/em&gt; discipline as part 1:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# state.py — the whole run in one model
from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field

THRESHOLD = 60   # score at or above this = &quot;worth applying&quot;
MAX_CALLS = 3    # 1 first try + 2 repairs — the budget from part 1


class AgentState(BaseModel):
    &quot;&quot;&quot;One run of the copilot, end to end.&quot;&quot;&quot;

    jd_text: str = &quot;&quot;
    context: str = &quot;&quot;              # resume chunks, from part 2
    last_raw: str = &quot;&quot;             # what the model last said, verbatim
    error: str | None = None       # latest parse failure, if any
    attempt: int = 0               # how many LLM calls so far

    match_score: int | None = None
    missing_skills: list[str] = Field(default_factory=list)
    verdict: Literal[&quot;match&quot;, &quot;no_match&quot;, &quot;error&quot;] | None = None
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;!-- NOTE TO V: `THRESHOLD = 60` and `MAX_CALLS = 3` are illustrative constants; swap in your real repo values. --&amp;gt;
&lt;p&gt;The rule that makes a graph a graph: &lt;strong&gt;a node never calls another node.&lt;/strong&gt; It receives state, does one job, and returns a &lt;code&gt;dict&lt;/code&gt; with &lt;em&gt;only what changed&lt;/em&gt; — a patch. LangGraph merges the patch and hands state to the next node. &lt;code&gt;retrieve&lt;/code&gt; fills &lt;code&gt;context&lt;/code&gt;; &lt;code&gt;judge&lt;/code&gt; fills the score fields; nobody knows about anybody else.&lt;/p&gt;
&lt;p&gt;Fields without a custom reducer are &lt;strong&gt;last-writer-wins&lt;/strong&gt; — I proved it to myself before trusting it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def note_a(state: AgentState) -&amp;gt; dict:
    return {&quot;missing_skills&quot;: [&quot;Python&quot;]}

def note_b(state: AgentState) -&amp;gt; dict:
    return {&quot;missing_skills&quot;: [&quot;Terraform&quot;]}

# a -&amp;gt; b in that order: missing_skills == [&quot;Terraform&quot;] — b overwrote a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For my state that's correct by design: every field is overwrite-on-purpose, so zero reducers. If a list should &lt;em&gt;accumulate&lt;/em&gt; across nodes, opt in with &lt;code&gt;Annotated&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Annotated


def merge_lists(left: list[str] | None, right: list[str] | None) -&amp;gt; list[str]:
    if left is None:
        return right or []
    if right is None:
        return left
    return list(left) + [x for x in right if x not in left]


class AccumState(BaseModel):
    missing_skills: Annotated[list[str], merge_lists] = Field(default_factory=list)

# two nodes appending &quot;Python&quot; then &quot;Terraform&quot; -&amp;gt; [&quot;Python&quot;, &quot;Terraform&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And why pydantic for state instead of the &lt;code&gt;TypedDict&lt;/code&gt; every tutorial starts with? A patch is data from somewhere else, and at merge time nobody type-checks a plain dict:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-pycon&quot;&gt;# TypedDict state, merged by dict spread — type-checkers scream, Python shrugs:
&amp;gt;&amp;gt;&amp;gt; {**{&quot;attempt&quot;: 0, &quot;verdict&quot;: None}, &quot;verdict&quot;: 21, &quot;attempt&quot;: &quot;two&quot;}
{&quot;attempt&quot;: &quot;two&quot;, &quot;verdict&quot;: 21}          # garbage, happily stored

# pydantic model:
&amp;gt;&amp;gt;&amp;gt; PS(attempt=&quot;two&quot;, verdict=21)
pydantic.ValidationError: attempt — Input should be a valid integer...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same lesson as part 1: &lt;strong&gt;shape is a contract, and contracts live in pydantic, not in comments.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-the-graph-is-a-handful-of-declared-edges&quot;&gt;2. The graph is a handful of declared edges&lt;/h3&gt;
&lt;p&gt;Here is the entire &lt;code&gt;graph.py&lt;/code&gt; — wiring, not logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# graph.py
from langgraph.graph import END, START, StateGraph

from nodes import Chat, fail_node, judge_node, retrieve_node, route_after_parse
from state import AgentState


def build_graph(chat: Chat):
    g = StateGraph(AgentState)

    g.add_node(&quot;retrieve&quot;, retrieve_node)
    g.add_node(&quot;judge&quot;, lambda s: judge_node(s, chat))  # closure: node gets the LLM client
    g.add_node(&quot;fail&quot;, fail_node)

    g.add_edge(START, &quot;retrieve&quot;)
    g.add_edge(&quot;retrieve&quot;, &quot;judge&quot;)

    g.add_conditional_edges(
        &quot;judge&quot;,
        route_after_parse,          # returns &quot;ok&quot; | &quot;retry&quot; | &quot;give_up&quot;
        {&quot;ok&quot;: END, &quot;retry&quot;: &quot;judge&quot;, &quot;give_up&quot;: &quot;fail&quot;},
    )
    g.add_edge(&quot;fail&quot;, END)

    return g.compile()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coming from React, this shape clicked instantly: it's a component tree. Components don't import each other to call each other — data flows down a declared structure, and the structure decides what happens next.&lt;/p&gt;
&amp;lt;!-- NOTE TO V: align node names (`retrieve` / `judge` / `fail`) and the `chat(prompt)` thin-client signature with your repo; `chat` is the same one-call LLM wrapper the structured-output article assumed. --&amp;gt;
&lt;p&gt;The &lt;code&gt;lambda&lt;/code&gt; is deliberate: the graph is built once with a concrete LLM client, and no node ever imports a provider. That's what makes the graph testable — hand it a fake &lt;code&gt;chat&lt;/code&gt; and the whole flow runs with zero network. Which is exactly how I caught the bug.&lt;/p&gt;
&lt;h3 id=&quot;3-all-routing-logic-in-one-boring-function-and-the-wire-that-said-error-to-everything&quot;&gt;3. All routing logic in one boring function — and the wire that said &quot;error&quot; to everything&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;add_conditional_edges&lt;/code&gt; takes a &lt;em&gt;router&lt;/em&gt;: a pure function from state to a string, plus a mapping from strings to destinations. Mine is twelve lines and holds the entire loop policy of the agent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# nodes.py
from state import MAX_CALLS


def route_after_parse(state: AgentState) -&amp;gt; str:
    &quot;&quot;&quot;All loop logic in one boring function.&quot;&quot;&quot;
    if state.error is None:
        return &quot;ok&quot;
    if state.attempt &amp;lt; MAX_CALLS:
        return &quot;retry&quot;
    return &quot;give_up&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compare with my first wiring attempt, written &lt;em&gt;because&lt;/em&gt; the router concept felt foreign:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;g.add_edge(&quot;retrieve&quot;, &quot;judge&quot;)
g.add_edge(&quot;judge&quot;, &quot;fail&quot;)     # &quot;temporarily — I'll wire the real branches next&quot;
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;!-- NOTE TO V: adjust this anecdote to what actually happened in your repo; keep the lesson either way. --&amp;gt;
&lt;p&gt;Then I ran it — with a fake &lt;code&gt;chat&lt;/code&gt; returning a &lt;em&gt;perfect&lt;/em&gt; JSON verdict. Result: &lt;code&gt;verdict = &quot;error&quot;&lt;/code&gt;. Every single job. The parse was flawless, the score valid, and the graph marched it into &lt;code&gt;fail_node&lt;/code&gt; anyway: I had declared exactly one outgoing edge from &lt;code&gt;judge&lt;/code&gt;, and a graph never &quot;falls through&quot;. Nothing raised, nothing warned — the state machine politely followed the only road I built:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;gt;&amp;gt;&amp;gt; # plain edge judge -&amp;gt; fail, no conditional edges, perfect JSON from the model
&amp;gt;&amp;gt;&amp;gt; app.invoke(AgentState(jd_text=&quot;junior&quot;))
verdict = &quot;error&quot;     # a valid parse, executed through the wrong edge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix was the conditional edges above, plus two habits that came with it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Route values are data, not flow control.&lt;/strong&gt; If the router returns a key that's not in the mapping, LangGraph doesn't guess — it throws &lt;code&gt;KeyError&lt;/code&gt; at runtime. I know because my typo did exactly that. An unmapped route crashes loudly; a wrongly-wired edge fails silently. I'll take the crash.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loops are declared, not nested.&lt;/strong&gt; &lt;code&gt;&quot;retry&quot;: &quot;judge&quot;&lt;/code&gt; makes a cycle in the graph. The loop exists as an edge, its budget lives in &lt;code&gt;MAX_CALLS&lt;/code&gt;, and its stop condition is one pure function I can unit-test without LLM, network, or shame.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shift, internalized: in nested functions, control flow was &lt;em&gt;who calls whom&lt;/em&gt; — invisible at a glance. In the graph, it's &lt;em&gt;data in the edges&lt;/em&gt; — visible in one screenful.&lt;/p&gt;
&lt;h3 id=&quot;4-closing-the-repair-loop-with-state-not-recursion&quot;&gt;4. Closing the repair loop with state, not recursion&lt;/h3&gt;
&lt;p&gt;Last piece: &lt;code&gt;judge_node&lt;/code&gt;, the only node that talks to the model. It reads state to decide &lt;em&gt;which&lt;/em&gt; prompt to send — first pass or repair — and returns a patch either way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# nodes.py
from schemas import ParseError, ParseOk, parse_match
from state import AgentState, THRESHOLD

MATCH_PROMPT = &quot;&quot;&quot;You are a skeptical technical recruiter.
Compare the job description to the candidate's resume excerpt.
Reply with ONLY a JSON object:
{{&quot;match_score&quot;: &amp;lt;int 0-100&amp;gt;, &quot;missing_skills&quot;: [&amp;lt;string, ...&amp;gt;]}}

JOB DESCRIPTION:
{jd}

RESUME EXCERPT:
{context}
&quot;&quot;&quot;

FIX_PROMPT = &quot;&quot;&quot;Your previous reply could not be parsed.

PREVIOUS REPLY:
{previous}

PARSER ERROR:
{error}

Reply with ONLY a valid JSON object:
{{&quot;match_score&quot;: &amp;lt;int 0-100&amp;gt;, &quot;missing_skills&quot;: [&amp;lt;string, ...&amp;gt;]}}
&quot;&quot;&quot;


def judge_node(state: AgentState, chat: Chat) -&amp;gt; dict:
    &quot;&quot;&quot;One LLM call, one parse, a patch of state. Loops are the graph's job now.&quot;&quot;&quot;
    if state.error is None:
        prompt = MATCH_PROMPT.format(jd=state.jd_text, context=state.context)
    else:
        prompt = FIX_PROMPT.format(previous=state.last_raw, error=state.error)

    raw = chat(prompt)
    result = parse_match(raw)          # the part-1 contract, unchanged

    patch: dict = {&quot;last_raw&quot;: raw, &quot;attempt&quot;: state.attempt + 1}
    if isinstance(result, ParseOk):
        score = result.data.match_score
        patch.update(
            match_score=score,
            missing_skills=result.data.missing_skills,
            verdict=&quot;match&quot; if score &amp;gt;= THRESHOLD else &quot;no_match&quot;,
            error=None,
        )
    else:
        patch.update(error=result.message)
    return patch


def fail_node(state: AgentState) -&amp;gt; dict:
    &quot;&quot;&quot;The model never produced valid JSON. Say so — as state, not a stack trace.&quot;&quot;&quot;
    return {&quot;verdict&quot;: &quot;error&quot;}
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;!-- NOTE TO V: these prompts are skeleton versions; align wording with your actual `MATCH_PROMPT`/`FIX_PROMPT` from the structured-output layer. --&amp;gt;
&lt;p&gt;Because &lt;code&gt;build_graph&lt;/code&gt; takes &lt;code&gt;chat&lt;/code&gt; as a parameter, the end-to-end test is a fake answer card — call one drifts into prose, call two complies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def flaky_llm():
    calls = {&quot;n&quot;: 0}

    def chat(prompt: str) -&amp;gt; str:
        calls[&quot;n&quot;] += 1
        if calls[&quot;n&quot;] == 1:
            return &quot;Sure! The match is quite good, I'd say around 70 out of 100.&quot;
        return '{&quot;match_score&quot;: 70, &quot;missing_skills&quot;: [&quot;Terraform&quot;]}'

    return chat


app = build_graph(flaky_llm())
final = app.invoke(AgentState(jd_text=&quot;Senior Frontend Engineer (React, TS)&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;verdict: match | score: 70 | attempts: 2 | error: None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prose landed in &lt;code&gt;last_raw&lt;/code&gt;, &lt;code&gt;parse_match&lt;/code&gt; filled &lt;code&gt;error&lt;/code&gt;, the router said &lt;code&gt;&quot;retry&quot;&lt;/code&gt;, and the second call got &lt;code&gt;FIX_PROMPT&lt;/code&gt; with the model's own prose quoted back at it — the part-1 trick, now &lt;em&gt;driven by edges instead of a while-loop&lt;/em&gt;. When the model never complies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;verdict: error | error: no JSON object found in model output | attempts: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No exception slicing through three stack frames. &lt;code&gt;fail_node&lt;/code&gt; writes &lt;code&gt;verdict: &quot;error&quot;&lt;/code&gt; and the graph ends — for an agent run overnight over forty postings, a run that &lt;em&gt;ends with a recorded failure&lt;/em&gt; beats ten runs that crashed and took the batch with them. Part 1's &quot;errors as data&quot;, promoted from one function to the shape of the whole system.&lt;/p&gt;
&amp;lt;!-- NOTE TO V: outputs above are from my fake-LLM smoke run (langgraph 1.2.15, pydantic 2.14.1); re-check against your repo's pinned versions. Worth re-confirming on your version: last-writer-wins merge and `KeyError` on an unmapped route. --&amp;gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;The first graph, in one sentence: &lt;strong&gt;state is a pydantic contract, nodes return patches, a router returns strings, loops live in edges.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Three things this React-dev-learning-Python took from it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Patches, not calls.&lt;/strong&gt; The merge is last-writer-wins by default; accumulators are opt-in via &lt;code&gt;Annotated&lt;/code&gt;. Every field in my state is overwrite-on-purpose — zero reducers, but verified behavior before trusting it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing is a pure function.&lt;/strong&gt; Twelve lines, no LLM, unit-testable — and an unmapped return value crashes with a &lt;code&gt;KeyError&lt;/code&gt;. That's the framework refusing to guess, and I've come to love it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Giving up is a node, not a throw.&lt;/strong&gt; &lt;code&gt;fail&lt;/code&gt; writes &lt;code&gt;verdict: &quot;error&quot;&lt;/code&gt; and the graph ends cleanly. The batch survives; the log tells the truth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the bug that taught me most: a graph executes exactly the edges you declared — my single &lt;code&gt;judge → fail&lt;/code&gt; wire turned perfect parses into errors with zero pushback from the framework. A state machine has no common sense. That's the feature.&lt;/p&gt;
&lt;p&gt;Next Tuesday, part 4: evals. The judge says 72 — but is 72 &lt;em&gt;right&lt;/em&gt;? Fixtures of job descriptions with expected score ranges, a negative case that must score LOW (a Python-backend posting should &lt;em&gt;not&lt;/em&gt; match a frontend resume), and the first time JobCopilot tells me I'm wrong.&lt;/p&gt;
&lt;p&gt;Follow if you want to watch the rest of the copilot get built — including the parts that break. Build log + daily code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://t.me/novamind_hub&quot;&gt;t.me/novamind_hub&lt;/a&gt;. Part 1, in case you missed it: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264&quot;&gt;Stop prompting for valid JSON: an LLM output layer that does not break&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Do you build agent graphs with a framework or hand-roll the loop? And what's the first thing your graph did that you didn't expect?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32792/stop-calling-functions-my-first-langgraph-graph-where-state-does-the-calling</guid>
<pubDate>Wed, 28 Oct 2026 09:39:31 +0000</pubDate>
</item>
<item>
<title>How to Scrape Kick with Python: Channel Stats, Live Viewers, Clips, and a SQLite Tracker (2026)</title>
<link>https://coderlegion.com/32791/how-to-scrape-kick-with-python-channel-stats-live-viewers-clips-and-a-sqlite-tracker-2026</link>
<description>&lt;p&gt;Kick has turned into the second streaming platform a lot of people actually watch. Big creators moved over, Slots and IRL took off, and anyone in marketing, esports, or creator analytics now gets the same questions about Kick that they used to get about Twitch. How many followers does this streamer have? Are they live right now, and with how many viewers? Which clips are taking off? Which categories are growing?&lt;/p&gt;
&lt;p&gt;You can answer most of that with a short Python script. In this tutorial we'll build one with &lt;code&gt;requests&lt;/code&gt; and the standard library's &lt;code&gt;sqlite3&lt;/code&gt;. It:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads Kick's robots.txt, including its query-string rules&lt;/li&gt;
&lt;li&gt;Pulls channel stats: followers, verification, live status, current viewers, category&lt;/li&gt;
&lt;li&gt;Lists past broadcasts (VODs) and clips, with cursor pagination&lt;/li&gt;
&lt;li&gt;Ranks categories by live viewers&lt;/li&gt;
&lt;li&gt;Saves snapshots to SQLite and reports follower growth and peak viewers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everything below ran against the live site on October 28, 2026, from a cloud server in a datacenter. The outputs are real. The snippets all come from one file, &lt;code&gt;kick_scraper.py&lt;/code&gt; (about 320 lines), with a small command-line interface: &lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;vods&lt;/code&gt;, &lt;code&gt;clips&lt;/code&gt;, &lt;code&gt;clip&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt;, &lt;code&gt;track&lt;/code&gt;, and &lt;code&gt;report&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-kick-api-situation-official-vs-what-the-website-uses&quot;&gt;The Kick API situation: official vs. what the website uses&lt;/h3&gt;
&lt;p&gt;When people search for &quot;kick api,&quot; they usually mean one of two things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The official Kick API&lt;/strong&gt; (&lt;code&gt;https://api.kick.com/public/v1/...&lt;/code&gt;, documented at docs.kick.com) is a real, supported developer API. You register an app and get an OAuth token, either a user token or an app token through the client-credentials flow, then call endpoints like &lt;code&gt;GET /public/v1/channels?slug=xqc&lt;/code&gt;. Without a token, every endpoint answers &lt;code&gt;401 {&quot;message&quot;:&quot;Unauthorized&quot;}&lt;/code&gt;. I tried it. It's built for bots and integrations: chat, events, updating your own stream. Its channel object covers the category, stream title, live status, and &lt;code&gt;viewer_count&lt;/code&gt;. It does &lt;strong&gt;not&lt;/strong&gt; include follower counts, and it has no endpoints for clips or a channel's past broadcasts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The website's own JSON API&lt;/strong&gt; (&lt;code&gt;https://kick.com/api/v1/...&lt;/code&gt; and &lt;code&gt;/api/v2/...&lt;/code&gt;) is what kick.com's frontend calls to render channel pages. It's undocumented and needs no login. It also covers exactly the gaps above: followers, VOD history, clips, and category viewer totals. This tutorial uses it, carefully.&lt;/p&gt;
&lt;p&gt;If the official API covers your use case, use it. It's the stable, sanctioned option. Everything below is for the public numbers it leaves out.&lt;/p&gt;
&lt;h3 id=&quot;step-0-robots-txt-has-rules-about-query-strings&quot;&gt;Step 0: robots.txt has rules about query strings&lt;/h3&gt;
&lt;p&gt;This is Kick's robots.txt today, trimmed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User-Agent: *
Disallow: /login$
Disallow: /oauth/
...
Disallow: /*?*sort=
Disallow: /*?*languages=
Disallow: /*?*tags=
Disallow: /*?*clip=
Disallow: /*?*query=
Disallow: /*?*range=
Disallow: /*?*filter=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things stand out. First, &lt;code&gt;/api/&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; disallowed. Second, most of the rules target &lt;strong&gt;query parameters&lt;/strong&gt;: any URL with &lt;code&gt;sort=&lt;/code&gt;, &lt;code&gt;query=&lt;/code&gt;, &lt;code&gt;filter=&lt;/code&gt; and so on. So the same path can be fine with one query string and off-limits with another. The clips endpoint accepts a &lt;code&gt;sort=view&lt;/code&gt; parameter (the website uses it for &quot;most viewed&quot;). By these rules, you shouldn't send it.&lt;/p&gt;
&lt;p&gt;Python's built-in &lt;code&gt;urllib.robotparser&lt;/code&gt; doesn't handle &lt;code&gt;*&lt;/code&gt; wildcards, so it would get almost every rule in this file wrong. Here's a small matcher that uses Google's &quot;longest matching rule wins&quot; logic, run against the full path plus query string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def load_robots():
    txt = session.get(BASE + &quot;/robots.txt&quot;, timeout=TIMEOUT).text
    rules, active = [], False
    for line in txt.splitlines():
        line = line.split(&quot;#&quot;)[0].strip()
        if &quot;:&quot; not in line:
            continue
        key, val = (s.strip() for s in line.split(&quot;:&quot;, 1))
        key = key.lower()
        if key == &quot;user-agent&quot;:
            active = val == &quot;*&quot;
        elif active and key in (&quot;allow&quot;, &quot;disallow&quot;) and val:
            rx = &quot;^&quot; + re.escape(val).replace(r&quot;\*&quot;, &quot;.*&quot;)
            rx = rx[:-2] + &quot;$&quot; if rx.endswith(r&quot;\$&quot;) else rx
            rules.append((key, val, re.compile(rx)))
    return rules

def allowed(path_and_query, rules):
    hits = [(len(raw), kind == &quot;allow&quot;) for kind, raw, rx in rules if rx.match(path_and_query)]
    # longest rule wins; on a tie, Allow beats Disallow; no match means allowed
    return max(hits)[1] if hits else True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;python3 kick_scraper.py robots&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/xqc                                             allowed=True
/api/v2/channels/xqc                             allowed=True
/api/v2/channels/xqc/clips                       allowed=True
/api/v2/channels/xqc/clips?sort=view&amp;amp;time=all    allowed=False
/api/v2/channels/xqc/clips?cursor=abc            allowed=True
/browse?query=gta                                allowed=False
/login                                           allowed=False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's why the scraper below lists clips in the default order (newest first) and does its own ranking. robots.txt is a crawl policy, not a license, so also read Kick's terms of service before you build anything commercial on this. My own rules for this project: no login, public counters only, nothing about individual viewers, and no more than one request every 1.5 seconds.&lt;/p&gt;
&lt;h3 id=&quot;step-1-a-polite-fetcher&quot;&gt;Step 1: A polite fetcher&lt;/h3&gt;
&lt;p&gt;Kick sits behind Cloudflare. While testing, I made 40 back-to-back requests to a channel endpoint and got 40 &lt;code&gt;200&lt;/code&gt;s. The default curl and &lt;code&gt;Python-urllib&lt;/code&gt; user agents got through too. That doesn't mean it will always work. The main website (&lt;code&gt;/xqc&lt;/code&gt;, the HTML page) behaves differently from the JSON endpoints, and Cloudflare can start serving its &quot;Just a moment...&quot; challenge page at any time. Write the fetcher so it notices that and stops, and never try to solve the challenge yourself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import requests, time, sys

BASE = &quot;https://kick.com&quot;
DELAY = 1.5
TIMEOUT = 20

session = requests.Session()
session.headers.update({
    &quot;User-Agent&quot;: &quot;Mozilla/5.0 (compatible; kick-research-script/1.0; +mailto:*Emails are not allowed*)&quot;,
    &quot;Accept&quot;: &quot;application/json&quot;,
})
_last = 0.0

class NotFound(Exception): pass
class Blocked(Exception): pass

def get_json(path, params=None, tries=4):
    global _last
    url = BASE + path
    for attempt in range(tries):
        time.sleep(max(0.0, DELAY - (time.time() - _last)))
        _last = time.time()
        r = session.get(url, params=params, timeout=TIMEOUT)
        ctype = r.headers.get(&quot;content-type&quot;, &quot;&quot;)
        if r.status_code == 200 and &quot;json&quot; in ctype:
            return r.json()
        if r.status_code == 404:
            raise NotFound(f&quot;404 for {r.url}&quot;)
        if r.status_code in (429, 500, 502, 503, 504):
            wait = int(r.headers.get(&quot;Retry-After&quot;) or 0) or 5 * 2 ** attempt
            time.sleep(wait)
            continue
        if r.status_code == 403 or &quot;Just a moment&quot; in r.text[:2000]:
            raise Blocked(f&quot;Cloudflare said no ({r.status_code}) on {r.url}&quot;)
        raise RuntimeError(f&quot;unexpected {r.status_code} {ctype} for {r.url}&quot;)
    raise RuntimeError(f&quot;gave up on {url}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking &lt;code&gt;content-type&lt;/code&gt; matters. A Cloudflare challenge page is HTML, sometimes with status 403 and sometimes with 503. If you call &lt;code&gt;r.json()&lt;/code&gt; without checking, you get a confusing &lt;code&gt;JSONDecodeError&lt;/code&gt; instead of a clear &quot;you're blocked&quot; message.&lt;/p&gt;
&lt;h3 id=&quot;step-2-channel-stats-and-live-status&quot;&gt;Step 2: Channel stats and live status&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GET /api/v2/channels/&amp;lt;slug&amp;gt;&lt;/code&gt; returns about 7.7 KB of JSON: the channel, the owner's public profile, the current livestream (or &lt;code&gt;null&lt;/code&gt;), subscriber badges, chatroom settings, and more. We keep the useful parts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def to_int(v):
    try:
        return int(v)
    except (TypeError, ValueError):
        return None

def channel(slug):
    d = get_json(f&quot;/api/v2/channels/{slug}&quot;)
    live = d.get(&quot;livestream&quot;) or None
    user = d.get(&quot;user&quot;) or {}
    return {
        &quot;slug&quot;: d[&quot;slug&quot;],
        &quot;channel_id&quot;: d[&quot;id&quot;],
        &quot;username&quot;: user.get(&quot;username&quot;),
        &quot;followers&quot;: to_int(d.get(&quot;followers_count&quot;)),
        &quot;verified&quot;: bool(d.get(&quot;verified&quot;)),
        &quot;socials&quot;: {k: user[k] for k in (&quot;instagram&quot;, &quot;twitter&quot;, &quot;youtube&quot;, &quot;tiktok&quot;, &quot;discord&quot;) if user.get(k)},
        &quot;recent_categories&quot;: [c[&quot;name&quot;] for c in d.get(&quot;recent_categories&quot;) or []],
        &quot;is_live&quot;: bool(live and live.get(&quot;is_live&quot;)),
        &quot;viewers&quot;: live.get(&quot;viewer_count&quot;) if live else None,
        &quot;stream_title&quot;: live.get(&quot;session_title&quot;) if live else None,
        &quot;stream_category&quot;: (live.get(&quot;categories&quot;) or [{}])[0].get(&quot;name&quot;) if live else None,
        &quot;live_since&quot;: live.get(&quot;start_time&quot;) if live else None,
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;python3 kick_scraper.py channel xqc&lt;/code&gt; (trimmed):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
 &quot;slug&quot;: &quot;xqc&quot;,
 &quot;channel_id&quot;: 668,
 &quot;username&quot;: &quot;xQc&quot;,
 &quot;followers&quot;: 1392316,
 &quot;verified&quot;: true,
 &quot;recent_categories&quot;: [&quot;Grand Theft Auto V (GTA)&quot;, &quot;Just Chatting&quot;],
 &quot;is_live&quot;: true,
 &quot;viewers&quot;: 9008,
 &quot;stream_category&quot;: &quot;Grand Theft Auto V (GTA)&quot;,
 &quot;live_since&quot;: &quot;2026-10-27 21:02:50&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gotchas from this one endpoint:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;followers_count&lt;/code&gt; is a string&lt;/strong&gt; here (&lt;code&gt;&quot;1392298&quot;&lt;/code&gt;). The search endpoint returns the same field as an integer. Normalize it in one place (&lt;code&gt;to_int&lt;/code&gt;) or your SQL comparisons will quietly sort &lt;code&gt;&quot;99&quot;&lt;/code&gt; above &lt;code&gt;&quot;1000&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The response includes a signed &lt;code&gt;playback_url&lt;/code&gt;&lt;/strong&gt; with a stream token in it. Don't store it. It expires, and it has nothing to do with analytics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The response is cached.&lt;/strong&gt; It comes back with &lt;code&gt;cache-control: public, max-age=15&lt;/code&gt; and &lt;code&gt;cf-cache-status: HIT&lt;/code&gt;. Viewer counts from it can be up to 15 seconds old. That's fine for a tracker, but don't poll every two seconds expecting fresh numbers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unknown or renamed channels&lt;/strong&gt; return &lt;code&gt;404 {&quot;message&quot;:&quot;Channel not found.&quot;}&lt;/code&gt;. Treat that as a signal in your tracker, not as a crash. Streamers rename channels, and channels get banned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a live check alone, there's a lighter endpoint, &lt;code&gt;/api/v2/channels/&amp;lt;slug&amp;gt;/livestream&lt;/code&gt;. It returns &lt;code&gt;{&quot;data&quot;: null}&lt;/code&gt; when the channel is offline, and it's &lt;strong&gt;not&lt;/strong&gt; cached (&lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt;). The tracker uses it for the viewer count.&lt;/p&gt;
&lt;h3 id=&quot;step-3-vods-past-broadcasts-&quot;&gt;Step 3: VODs (past broadcasts)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/api/v2/channels/&amp;lt;slug&amp;gt;/videos&lt;/code&gt; returns a channel's recent broadcasts as a plain list (25 for xQc today), newest first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def kick_time(s):
    if not s:
        return None
    s = s.replace(&quot;Z&quot;, &quot;+00:00&quot;)
    if &quot;T&quot; not in s:
        s = s.replace(&quot; &quot;, &quot;T&quot;) + &quot;+00:00&quot;
    return datetime.fromisoformat(s).astimezone(timezone.utc)

def vods(slug, limit=10):
    out = []
    for v in get_json(f&quot;/api/v2/channels/{slug}/videos&quot;)[:limit]:
        start = kick_time(v.get(&quot;start_time&quot;))
        out.append({
            &quot;uuid&quot;: (v.get(&quot;video&quot;) or {}).get(&quot;uuid&quot;),
            &quot;title&quot;: v.get(&quot;session_title&quot;),
            &quot;started_utc&quot;: start.isoformat() if start else None,
            &quot;hours&quot;: round((v.get(&quot;duration&quot;) or 0) / 3_600_000, 2),
            &quot;views&quot;: v.get(&quot;views&quot;),
            &quot;still_live&quot;: v.get(&quot;is_live&quot;),
            &quot;category&quot;: (v.get(&quot;categories&quot;) or [{}])[0].get(&quot;name&quot;),
        })
    return out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;python3 kick_scraper.py vods xqc --limit 6&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2026-10-27T21:02    0.0h       345 views  Grand Theft Auto V (GTA)
2026-10-26T20:59  13.03h    46,406 views  Just Chatting
2026-10-24T20:50  11.86h    53,208 views  Just Chatting
2026-10-23T21:07  11.34h   246,210 views  Just Chatting
2026-10-22T19:04  13.09h    48,912 views  Just Chatting
2026-10-21T19:56  11.92h    56,135 views  Grand Theft Auto V (GTA)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is already a useful dataset: stream schedule, hours streamed per week, and which category each session was in. Pitfalls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The first row is the stream that's still running.&lt;/strong&gt; Its &lt;code&gt;duration&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;views&lt;/code&gt; is tiny (345 while 9,000 people were watching live), because &lt;code&gt;views&lt;/code&gt; counts &lt;strong&gt;replay views of the VOD&lt;/strong&gt;, not live viewers. Filter on &lt;code&gt;is_live&lt;/code&gt; before you compute averages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;duration&lt;/code&gt; is in milliseconds.&lt;/strong&gt; Clips (next step) use seconds. I got this wrong the first time and had xQc streaming 46,891,000-hour sessions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two timestamp formats.&lt;/strong&gt; Livestreams and VODs use &lt;code&gt;&quot;2026-10-27 21:02:50&quot;&lt;/code&gt;, with no time zone (it's UTC; it matches the &quot;live for 12 hours&quot; you see on the site). Clips use ISO 8601 with a &lt;code&gt;Z&lt;/code&gt;. &lt;code&gt;kick_time()&lt;/code&gt; normalizes both to timezone-aware UTC.&lt;/li&gt;
&lt;li&gt;Each VOD row also has a &lt;code&gt;viewer_count&lt;/code&gt;. Kick doesn't document what it means for a finished stream (peak? the last reading?), so I don't use it. Measure peaks yourself with snapshots (Step 6).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-4-clips-with-cursor-pagination&quot;&gt;Step 4: Clips with cursor pagination&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/api/v2/channels/&amp;lt;slug&amp;gt;/clips&lt;/code&gt; returns 20 clips per page plus a &lt;code&gt;nextCursor&lt;/code&gt;, which is a small JSON string like &lt;code&gt;{&quot;view&quot;:0,&quot;id&quot;:&quot;clip_01M60EX7...&quot;}&lt;/code&gt;. You pass it back as &lt;code&gt;?cursor=...&lt;/code&gt;. &lt;code&gt;requests&lt;/code&gt; handles the URL encoding.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def clips(slug, pages=1):
    out, seen, cursor = [], set(), None
    for _ in range(pages):
        d = get_json(f&quot;/api/v2/channels/{slug}/clips&quot;, {&quot;cursor&quot;: cursor} if cursor else None)
        for c in d.get(&quot;clips&quot;) or []:
            if c[&quot;id&quot;] in seen:
                continue
            seen.add(c[&quot;id&quot;])
            out.append({
                &quot;id&quot;: c[&quot;id&quot;],
                &quot;title&quot;: c.get(&quot;title&quot;),
                &quot;views&quot;: c.get(&quot;views&quot;),
                &quot;likes&quot;: c.get(&quot;likes&quot;),
                &quot;seconds&quot;: c.get(&quot;duration&quot;),
                &quot;created_utc&quot;: kick_time(c.get(&quot;created_at&quot;)).isoformat(timespec=&quot;seconds&quot;),
                &quot;category&quot;: (c.get(&quot;category&quot;) or {}).get(&quot;name&quot;),
                &quot;url&quot;: f&quot;{BASE}/{slug}/clips/{c['id']}&quot;,
            })
        cursor = d.get(&quot;nextCursor&quot;)
        if not cursor:
            break
    return out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;python3 kick_scraper.py clips xqc --pages 3&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;60 clips, newest 2026-10-28T08:48:37+00:00, oldest 2026-10-28T02:15:11+00:00
   201 views    0 likes   70s  2026-10-28T03:43  https://kick.com/xqc/clips/clip_01M609K1SMT5AQCC1J627KBVAM
    77 views    0 likes   27s  2026-10-28T05:53  https://kick.com/xqc/clips/clip_01M60H258MDK5Q2EPP5RSRN42S
    20 views    0 likes   79s  2026-10-28T03:29  https://kick.com/xqc/clips/clip_01M608V29RVXF537QPCK804NXK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sixty clips covered only about six and a half hours of a single stream. Big channels get clipped constantly, so &quot;every clip from last month&quot; means hundreds of pages. Pick a time window and stop paginating once &lt;code&gt;created_utc&lt;/code&gt; falls outside it.&lt;/p&gt;
&lt;p&gt;Things to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The list is cached for up to four hours&lt;/strong&gt; (&lt;code&gt;cache-control: public, max-age=14400&lt;/code&gt;). New clips and fresh view counts can lag behind the site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Some fields in the list are placeholders.&lt;/strong&gt; In the list response, &lt;code&gt;started_at&lt;/code&gt; came back as &lt;code&gt;0001-01-01T00:00:00Z&lt;/code&gt; and &lt;code&gt;vod_starts_at&lt;/code&gt; as &lt;code&gt;0&lt;/code&gt;. The single-clip endpoint &lt;code&gt;/api/v2/clips/&amp;lt;id&amp;gt;&lt;/code&gt; returns the real values (for the clip above, &lt;code&gt;vod_starts_at: 23046&lt;/code&gt;, which is 6.4 hours into the VOD). So if you need the timestamp inside the VOD, fetch each clip you care about individually:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def clip(clip_id):
    clip_id = clip_id.rstrip(&quot;/&quot;).split(&quot;/&quot;)[-1]
    c = get_json(f&quot;/api/v2/clips/{clip_id}&quot;)[&quot;clip&quot;]
    return {&quot;id&quot;: c[&quot;id&quot;], &quot;channel&quot;: (c.get(&quot;channel&quot;) or {}).get(&quot;slug&quot;),
            &quot;views&quot;: c.get(&quot;views&quot;), &quot;likes&quot;: c.get(&quot;likes&quot;), &quot;seconds&quot;: c.get(&quot;duration&quot;),
            &quot;vod_offset_s&quot;: c.get(&quot;vod_starts_at&quot;), &quot;created_utc&quot;: c.get(&quot;created_at&quot;)}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Each clip includes a &lt;code&gt;creator&lt;/code&gt; object: the &lt;strong&gt;viewer&lt;/strong&gt; who made the clip. That's an individual user, not the streamer, so the scraper drops it.&lt;/li&gt;
&lt;li&gt;Clip titles are free text from viewers. One of the top clips today was titled &lt;code&gt;&quot;.&quot;&lt;/code&gt;. Don't build a keyword report on clip titles and expect much.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-5-which-categories-are-hot-right-now&quot;&gt;Step 5: Which categories are hot right now&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/api/v1/subcategories&lt;/code&gt; is a standard Laravel-style paginated list (&lt;code&gt;current_page&lt;/code&gt;, &lt;code&gt;next_page_url&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;). Each item has a &lt;code&gt;viewers&lt;/code&gt; total, and the list comes back ordered by it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def categories(pages=1, per_page=25):
    out = []
    for p in range(1, pages + 1):
        d = get_json(&quot;/api/v1/subcategories&quot;, {&quot;limit&quot;: per_page, &quot;page&quot;: p})
        for c in d.get(&quot;data&quot;) or []:
            out.append({&quot;slug&quot;: c[&quot;slug&quot;], &quot;name&quot;: c[&quot;name&quot;], &quot;viewers&quot;: c.get(&quot;viewers&quot;)})
        if not d.get(&quot;next_page_url&quot;):
            break
    return out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At 09:00 UTC on a Wednesday:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  88,062  Just Chatting
  77,085  Grand Theft Auto V (GTA)
  49,870  IRL
  34,218  Slots &amp;amp; Casino
  32,938  PUBG Mobile
  26,039  Counter-Strike 2
  14,815  Call of Duty: Modern Warfare 4
  10,959  Music Stations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those totals swing a lot with time of day. GTA at 77k is mostly a handful of large roleplay streams. One streamer going offline can move a category by 10%. A single reading tells you very little, which brings us to the tracker.&lt;/p&gt;
&lt;h3 id=&quot;step-6-a-sqlite-tracker-for-followers-and-live-viewers&quot;&gt;Step 6: A SQLite tracker for followers and live viewers&lt;/h3&gt;
&lt;p&gt;Two tables: one row per channel per run, and one row per category per run. SQLite is plenty for this. A year of hourly snapshots for 50 channels is about 440,000 rows.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;SCHEMA = &quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS channel_snap (
  ts TEXT, slug TEXT, followers INTEGER, is_live INTEGER, viewers INTEGER,
  category TEXT, title TEXT, PRIMARY KEY (ts, slug));
CREATE TABLE IF NOT EXISTS category_snap (
  ts TEXT, slug TEXT, viewers INTEGER, PRIMARY KEY (ts, slug));
&quot;&quot;&quot;

def track(slugs, db, top_categories=25):
    con = sqlite3.connect(db)
    con.executescript(SCHEMA)
    ts = datetime.now(timezone.utc).isoformat(timespec=&quot;seconds&quot;)
    for s in slugs:
        try:
            c = channel(s)
        except NotFound:
            print(f&quot;  skip {s}: channel not found (renamed or banned?)&quot;, file=sys.stderr)
            continue
        if c[&quot;is_live&quot;]:
            ls = livestream(s)          # uncached, fresher viewer count
            if ls:
                c[&quot;viewers&quot;] = ls.get(&quot;viewer_count&quot;, c[&quot;viewers&quot;])
        con.execute(&quot;INSERT OR REPLACE INTO channel_snap VALUES (?,?,?,?,?,?,?)&quot;,
                    (ts, c[&quot;slug&quot;], c[&quot;followers&quot;], int(c[&quot;is_live&quot;]), c[&quot;viewers&quot;],
                     c[&quot;stream_category&quot;], c[&quot;stream_title&quot;]))
    for cat in categories(pages=1, per_page=top_categories):
        con.execute(&quot;INSERT OR REPLACE INTO category_snap VALUES (?,?,?)&quot;, (ts, cat[&quot;slug&quot;], cat[&quot;viewers&quot;]))
    con.commit()
    con.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The report uses window functions (SQLite 3.25+, which any current Python ships with) to compare each channel's first and latest snapshot and pull peak viewers and live ratio in one query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH r AS (
  SELECT slug, ts, followers, is_live, viewers,
         ROW_NUMBER() OVER (PARTITION BY slug ORDER BY ts)      AS first_n,
         ROW_NUMBER() OVER (PARTITION BY slug ORDER BY ts DESC) AS last_n,
         COUNT(*)     OVER (PARTITION BY slug)                  AS snaps,
         MAX(viewers) OVER (PARTITION BY slug)                  AS peak,
         SUM(is_live) OVER (PARTITION BY slug)                  AS live_snaps
  FROM channel_snap)
SELECT a.slug, a.followers, b.followers, a.ts, b.ts, b.snaps, b.peak, b.live_snaps
FROM r a JOIN r b ON a.slug = b.slug AND a.first_n = 1 AND b.last_n = 1
ORDER BY b.followers - a.followers DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tracked four big channels, three snapshots 15 minutes apart:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 kick_scraper.py track xqc adinross amouranth trainwreckstv --db kick.db
python3 kick_scraper.py report --db kick.db
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;channel           followers   change   per day  peak viewers  live/snaps
xqc               1,392,646     +324    +15364         8,989  3/3
adinross          2,765,006     +218    +10338             0  0/3
trainwreckstv       682,165      +19      +901             0  0/3
amouranth           283,381      +12      +569             0  0/3

category viewer swings (first vs latest snapshot):
  just-chatting                  88,062 -&amp;gt;    76,231  (-11,831)
  grand-theft-auto-v             77,085 -&amp;gt;    75,550  (-1,535)
  irl                            49,870 -&amp;gt;    47,120  (-2,750)
  slots                          34,218 -&amp;gt;    34,601  (+383)
  counter-strike-2               26,039 -&amp;gt;    30,788  (+4,749)
  pubg-mobile                    32,938 -&amp;gt;    22,344  (-10,594)
  dota-2                          9,488 -&amp;gt;    14,949  (+5,461)
  call-of-duty-modern-warfare-4   14,815 -&amp;gt;    14,278  (-537)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In 30 minutes, xQc gained 324 followers while live, and Adin Ross gained 218 while &lt;strong&gt;offline&lt;/strong&gt;. Follower growth doesn't stop when the stream ends, and that's exactly the kind of thing one snapshot can't show you. The &quot;+15,364 per day&quot; column is a naive extrapolation from a half-hour window during a live stream. It will overshoot, so wait for a few days of data before you trust the rate. &lt;code&gt;peak viewers&lt;/code&gt; is the highest of my three readings, not Kick's real peak. Sample more often if you care about peaks.&lt;/p&gt;
&lt;p&gt;The category section shows how noisy live totals are. Just Chatting lost 11,831 viewers and PUBG Mobile lost 10,594 in half an hour, while Dota 2 gained 5,461. Those are a few big streams starting and ending, not trends.&lt;/p&gt;
&lt;p&gt;For real use, run it from cron. Every 15 minutes is enough to catch peak viewers without hammering anything. Followers alone only need a daily run.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*/15 * * * * cd ~/kick &amp;amp;&amp;amp; python3 kick_scraper.py track xqc adinross amouranth trainwreckstv &amp;gt;&amp;gt; track.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With 15-minute snapshots you can answer real questions in SQL: average viewers per category per streamer, how many hours a week each channel is live, and whether a category switch (Just Chatting to GTA, for example) brings viewers or loses them.&lt;/p&gt;
&lt;h3 id=&quot;pitfalls-checklist&quot;&gt;Pitfalls checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Undocumented endpoints.&lt;/strong&gt; &lt;code&gt;/api/v1&lt;/code&gt; and &lt;code&gt;/api/v2&lt;/code&gt; are the website's internal API. Not every path exists in both versions (&lt;code&gt;/api/v1/channels/&amp;lt;slug&amp;gt;/videos&lt;/code&gt; returns 404, while the v2 path works). Check required keys and fail loudly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query-string robots rules.&lt;/strong&gt; Don't send &lt;code&gt;sort=&lt;/code&gt;, &lt;code&gt;query=&lt;/code&gt;, &lt;code&gt;filter=&lt;/code&gt; and similar parameters to kick.com. Sort locally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge caching.&lt;/strong&gt; Channel objects are cached for 15 seconds and clip lists for up to four hours. Only &lt;code&gt;/livestream&lt;/code&gt; is uncached. Know how old your data might be before you call something &quot;real time.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Units and types.&lt;/strong&gt; VOD &lt;code&gt;duration&lt;/code&gt; is in milliseconds and clip &lt;code&gt;duration&lt;/code&gt; is in seconds. &lt;code&gt;followers_count&lt;/code&gt; is sometimes a string. There are two timestamp formats, both UTC.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare.&lt;/strong&gt; If you get HTML instead of JSON, stop. Lower your rate, and never use challenge solvers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy.&lt;/strong&gt; Channel-level counters are fine. Chat logs, clip creators, and gift leaderboards are about individual viewers, so leave them out unless you have a clear, legitimate reason.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Official first.&lt;/strong&gt; If you only need live status, title, and category for a list of channels, the official Kick API's &lt;code&gt;GET /public/v1/channels&lt;/code&gt; (up to 50 slugs per call) with an app token is the sturdier choice.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-diy-stops-being-worth-it&quot;&gt;When DIY stops being worth it&lt;/h3&gt;
&lt;p&gt;The script above is about 300 lines and works well for tracking a few dozen channels. Where it gets tedious is refreshing individual clips at scale: one request per clip, the cached list endpoint lagging behind, and placeholder fields you only notice when your data looks wrong. For that, I run &lt;a rel=&quot;nofollow&quot; href=&quot;https://scrapeatlas.com&quot;&gt;ScrapeAtlas*&lt;/a&gt;. Its Kick endpoint is &lt;code&gt;GET /v1/kick/clip&lt;/code&gt;. You give it a clip URL (&lt;code&gt;kick.com/&amp;lt;channel&amp;gt;/clips/&amp;lt;clip_id&amp;gt;&lt;/code&gt;, or the legacy &lt;code&gt;?clip=&lt;/code&gt; form), and it returns that one public clip with current view and like counts, VOD offset, category, and channel, plus a &lt;code&gt;coverage&lt;/code&gt; block that tells you whether the result is complete:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os, requests

r = requests.get(
    &quot;https://api.scrapeatlas.com/v1/kick/clip&quot;,
    params={&quot;url&quot;: &quot;https://kick.com/xqc/clips/clip_01M609K1SMT5AQCC1J627KBVAM&quot;},
    headers={&quot;x-api-key&quot;: os.environ[&quot;SCRAPEATLAS_API_KEY&quot;]},
    timeout=30,
)
data = r.json()
print(data[&quot;clip&quot;][&quot;views&quot;], data[&quot;clip&quot;][&quot;likes&quot;], data[&quot;clip&quot;][&quot;vod_starts_at&quot;])
print(data[&quot;coverage&quot;])  # {&quot;source&quot;: &quot;public_api&quot;, &quot;complete&quot;: true, &quot;warnings&quot;: []}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To be clear, that's the only Kick endpoint: clips. Channel stats, VODs, and category viewers still come from the DIY code above (or the official API), and those parts are the cheap ones to run yourself.&lt;/p&gt;
&lt;p&gt;*I build it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32791/how-to-scrape-kick-with-python-channel-stats-live-viewers-clips-and-a-sqlite-tracker-2026</guid>
<pubDate>Wed, 28 Oct 2026 09:34:45 +0000</pubDate>
</item>
<item>
<title>MT5 Tick Size vs Point: A Test Harness for Price Distance</title>
<link>https://coderlegion.com/32775/mt5-tick-size-vs-point-a-test-harness-for-price-distance</link>
<description>&lt;p&gt;&lt;img src=&quot;https://guetaquant.com/images/riesgo-ejecucion-mt5-2026.webp&quot; alt=&quot;Illustration of uncertainty in execution&quot;&gt;&lt;/p&gt;
&lt;p&gt;The smallest quoted point and the minimum price change used as a trading tick can differ. If a program divides an exit distance by &lt;code&gt;SYMBOL_POINT&lt;/code&gt; but multiplies by the monetary value of a trading tick, the resulting loss estimate may use incompatible units. Test the units before testing position sizing.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Educational and simulation content only. This is not financial advice, intermediation or an investment recommendation. Trading can result in loss of capital.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;a-fictional-contract&quot;&gt;A fictional contract&lt;/h3&gt;
&lt;p&gt;Imagine a demo symbol with a point of 0.01 price units, a tick size of 0.05, and a loss-side tick value of 2 account-currency units per lot. A 0.50 price move spans 10 trading ticks, not 50. For 0.10 lots, a simplified linear estimate is 10 × 2 × 0.10 = 2 account-currency units. Substituting point for tick size would multiply that estimate by five. These inputs are fictional and do not describe any particular broker or instrument.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.mql5.com/en/docs/constants/environment_state/marketinfoconstants&quot;&gt;official MQL5 symbol reference&lt;/a&gt; defines &lt;code&gt;SYMBOL_POINT&lt;/code&gt;, &lt;code&gt;SYMBOL_TRADE_TICK_SIZE&lt;/code&gt;, and &lt;code&gt;SYMBOL_TRADE_TICK_VALUE_LOSS&lt;/code&gt; separately. Read actual symbol properties from the platform; do not assume that every symbol, account currency, or loss direction shares the same values.&lt;/p&gt;
&lt;h3 id=&quot;an-executable-unit-test&quot;&gt;An executable unit test&lt;/h3&gt;
&lt;p&gt;This example checks arithmetic under a deliberately narrow assumption: constant loss-side tick value and linear scaling with volume. It does not submit orders or model commissions, gaps or currency conversion changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from decimal import Decimal

def linear_estimate(entry, exit_price, tick_size, tick_value, lots):
    entry, exit_price, tick_size, tick_value, lots = map(
        lambda x: Decimal(str(x)),
        (entry, exit_price, tick_size, tick_value, lots),
    )
    if tick_size &amp;lt;= 0 or tick_value &amp;lt;= 0 or lots &amp;lt;= 0:
        raise ValueError(&quot;Tick size, tick value and lots must be positive&quot;)
    distance = abs(entry - exit_price)
    return distance / tick_size * tick_value * lots

assert linear_estimate(100, &quot;99.50&quot;, &quot;0.05&quot;, 2, &quot;0.10&quot;) == Decimal(&quot;2.00&quot;)
assert linear_estimate(100, &quot;100.50&quot;, &quot;0.05&quot;, 2, &quot;0.10&quot;) == Decimal(&quot;2.00&quot;)
assert linear_estimate(100, 100, &quot;0.05&quot;, 2, &quot;0.10&quot;) == 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The absolute distance checks units only. It does &lt;strong&gt;not&lt;/strong&gt; prove that the exit lies on the loss side: that depends on whether the simulated position is long or short. Check direction and valid prices separately. Prices and tick values can change; contract rules may make a simplified linear calculation unsuitable.&lt;/p&gt;
&lt;h3 id=&quot;compare-with-the-platform&quot;&gt;Compare with the platform&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.mql5.com/en/docs/trading/ordercalcprofit&quot;&gt;&lt;code&gt;OrderCalcProfit&lt;/code&gt;&lt;/a&gt; estimates the result in the account currency for an order type, symbol, volume and specified open and close prices. In a demo environment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Record symbol, account currency, direction, open price, exit price, volume, point, tick size and loss-side tick value.&lt;/li&gt;
&lt;li&gt;Check the return value of &lt;code&gt;OrderCalcProfit&lt;/code&gt;. If it fails, record the platform error and reject the estimate; never interpret failure as zero.&lt;/li&gt;
&lt;li&gt;Compare the signed result with the simplified calculation, using the same prices and volume. Investigate discrepancies rather than silently taking the smaller number.&lt;/li&gt;
&lt;li&gt;Repeat for long and short positions, multiple symbols and the broker's allowed volume increments. Keep fixtures and platform version with the results.&lt;/li&gt;
&lt;li&gt;Account separately for known costs, execution gaps and contract-specific rules. An estimate at the requested exit price is not a guaranteed loss limit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;MT5 also specifies minimum, maximum and step sizes for volumes. Those constraints are a separate test; see our &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/27300/the-silent-position-sizer-bug-when-accounts-are-small-your-risk-inadvertently-doubles&quot;&gt;earlier position-sizing article&lt;/a&gt; for the minimum-volume edge case. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://guetaquant.com/herramientas/mt5-position-sizer/?utm_source=coderlegion&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=mql5_risk_architecture&quot;&gt;Gueta Quant position sizer&lt;/a&gt; provides an educational comparison, not a guarantee of execution.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32775/mt5-tick-size-vs-point-a-test-harness-for-price-distance</guid>
<pubDate>Wed, 28 Oct 2026 06:37:05 +0000</pubDate>
</item>
<item>
<title>How to Protect a Flask or Django App When the WAF Is a Reverse Proxy</title>
<link>https://coderlegion.com/32770/how-to-protect-a-flask-or-django-app-when-the-waf-is-a-reverse-proxy</link>
<description>&lt;p&gt;Python teams usually start web security inside the framework. Django has auth, CSRF middleware, and an ORM. Flask has signed sessions, request hooks, and whatever validators you added. Those controls still matter. They are not the same thing as a web application firewall sitting on the HTTP path. If the question is how to protect a Python web app such as Flask or Django with a WAF, the SafeLine documents support a placement decision, not a language plugin. SafeLine is a self-hosted WAF that filters and monitors HTTP traffic between a web application and the Internet, and it works as a reverse proxy so clients reach the WAF before they reach the server (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The application guide says the same thing in operational terms: SafeLine receives traffic for the original site, cleans malicious traffic, and forwards safe traffic to the upstream application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Those pages do not describe a pip package, a Flask extension, or a Django middleware setting. Do not invent that integration. Decide whether a reverse-proxy shield fits the app you already run.&lt;/p&gt;
&lt;p&gt;Start with the entry path, not the view function. Write down the public hostname, the port clients use, and the real upstream that should stay private. In the SafeLine console, an application record takes a domain, hostname, or IP, a listen port such as 80 or 443, and an upstream address. HTTPS apps use the SSL option (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). After that, the domain must resolve to the SafeLine server. A Gunicorn process, a uWSGI socket, or an existing origin Nginx can remain the upstream. The change is that the public door moves. If a load balancer already terminates TLS, map that hop explicitly. Two public doors means the proxy is optional, and an optional WAF does not protect the app.&lt;/p&gt;
&lt;p&gt;Then check the host before you promise a cutover. The deploy guide requires Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, a CPU with ssse3, and at least 1 core, 1 GB of memory, and 5 GB of disk (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). The same page states that a SafeLine install on ARM requires a Pro license and that the Personal Edition is not supported on ARM. A small Flask demo on a laptop does not waive those constraints. If the host fails the check, fix the platform first. Filtering rules will not help if the proxy cannot run.&lt;/p&gt;
&lt;p&gt;Match the documented attack list to routes you actually expose. The introduction names SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, bruteforce, HTTP flood, and bot abuse (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). A Django admin search box, a Flask query parameter, a file download, and a webhook that fetches a URL are different code paths, but they are still HTTP. The proxy is useful because it inspects that traffic before the view runs. It is not useful as a substitute for parameterized queries, template escaping, or a view that shells out to a user-controlled string. Keep those fixes in the app.&lt;/p&gt;
&lt;p&gt;The same introduction lists core capabilities beyond signature-style blocking: defenses for web attacks, proactive bot abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The feature list then describes rate limiting for traffic that exceeds defined limits, including DoS, bruteforce attempts, traffic surges, and other abuse. It also describes a CAPTCHA challenge intended to allow human users while blocking crawlers and bots, an authentication challenge that requires a password, and dynamic protection that encrypts HTML and JS on each visit when it is turned on. Treat those as perimeter switches in the WAF, not as Django settings or Flask blueprints. The supplied pages do not document numeric thresholds, so do not copy invented limits into a runbook. Turn a control on, watch real traffic, and keep the strictest control off any path you have not tested.&lt;/p&gt;
&lt;p&gt;A published evaluation on 33,669 samples is the only benchmark figure these sources provide. In that table, the SafeLine Balance row shows 71.65% detection, a 0.07% false positive rate, and 99.45% accuracy. The SafeLine Strict row shows 76.17% detection, a 0.22% false positive rate, and 99.38% accuracy (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Use the table as a published comparison, not as a guarantee for your login form or JSON API. The Strict row detects more and also shows a higher false positive figure. That is a reason to replay ordinary browser flows, API clients, and file uploads after any policy change.&lt;/p&gt;
&lt;p&gt;Prove the proxy with your own hostname. The application guide publishes sample probes for SQL injection, XSS, path traversal, code injection, and XXE, and it says a block page means the attack was stopped. Details then appear in the console attack list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Run those patterns only against an application you are authorized to test, with the sample host replaced by your domain. A block proves the request passed through SafeLine. It does not prove that every Django permission check or Flask webhook is correct. Follow with a clean login, a normal form post, and a known-good API call. If a legitimate request is blocked, inspect the detail view before you weaken the whole site.&lt;/p&gt;
&lt;p&gt;The decision is complete when four statements are true. The app is reached over HTTP or HTTPS. One public name points at SafeLine, and the upstream is the real Flask or Django origin. The host meets the documented minimums, including the ARM license note if that architecture applies. Someone owns the attack list after cutover. If one statement is false, harden the Python app and wait. A reverse proxy is the supported path. A framework plugin is not.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-ship-a-flask-or-django-integration-&quot;&gt;Does SafeLine ship a Flask or Django integration?&lt;/h4&gt;
&lt;p&gt;No. The install and application guides describe a reverse-proxy WAF with a domain, a listen port, and an upstream. They do not document a Python package or a framework setting (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;which-request-classes-should-a-python-team-map-first-&quot;&gt;Which request classes should a Python team map first?&lt;/h4&gt;
&lt;p&gt;Start with the attacks named in the product introduction: SQL injection, XSS, code injection, command injection, SSRF, path traversal, bruteforce, HTTP flood, and bot abuse are the ones most teams can map to a route (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;are-rate-limiting-and-captcha-part-of-the-app-code-&quot;&gt;Are rate limiting and CAPTCHA part of the app code?&lt;/h4&gt;
&lt;p&gt;No. Rate limiting, CAPTCHA challenge, authentication challenge, and dynamic HTML and JS protection are WAF features described on the product page (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Keep CSRF, sessions, and permissions in Flask or Django.&lt;/p&gt;
&lt;h4 id=&quot;what-proves-the-cutover-worked-&quot;&gt;What proves the cutover worked?&lt;/h4&gt;
&lt;p&gt;Point the domain at the SafeLine server, replay the documented probe patterns against your own site, expect a block page, and open the attack detail in the console (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Then confirm normal user and API traffic still passes.&lt;/p&gt;
&lt;h4 id=&quot;what-host-checks-are-mandatory-&quot;&gt;What host checks are mandatory?&lt;/h4&gt;
&lt;p&gt;Linux, x86_64 or arm64, Docker 20.10.14 or newer, Docker Compose 2.0.0 or newer, ssse3, and at least 1 CPU core, 1 GB memory, and 5 GB disk. ARM also requires a Pro license, and the Personal Edition is not supported there (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32770/how-to-protect-a-flask-or-django-app-when-the-waf-is-a-reverse-proxy</guid>
<pubDate>Wed, 28 Oct 2026 02:30:19 +0000</pubDate>
</item>
<item>
<title>How to Decide If a Flask or Django App Needs a WAF</title>
<link>https://coderlegion.com/32769/how-to-decide-if-a-flask-or-django-app-needs-a-waf</link>
<description>&lt;p&gt;Protecting a Flask or Django application with a WAF is a placement decision, not a framework-plugin decision. The SafeLine introduction describes a self-hosted web application firewall that filters and monitors HTTP traffic between a web application and the Internet (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Those pages do not name Flask, Django, WSGI, or ASGI, and they do not document a Python package, middleware class, or SDK. If the requirement is an in-process integration, these sources do not support that promise. What they do support is a reverse-proxy checklist: confirm the host, put inspection in front of the existing process, forward only accepted traffic upstream, and prove a block before calling the app protected.&lt;/p&gt;
&lt;p&gt;SafeLine is documented as an HTTP and HTTPS reverse proxy developed on nginx. It receives traffic for the original site, cleans malicious attack traffic, and forwards safe traffic to the original application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). The same introduction distinguishes this from a forward proxy. A forward proxy hides a client. This WAF sits in front of the server so clients pass through it before they reach the application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). For Python, the useful reading is narrow. The Flask or Django process remains the upstream. The language of that process is not a documented configuration field.&lt;/p&gt;
&lt;h3 id=&quot;placement-checks&quot;&gt;Placement checks&lt;/h3&gt;
&lt;p&gt;Start with exposure, not with application code. In the web console, adding an application asks for Domain, Port, and Upstream. Domain may be a domain name, a hostname, or an IP address. Port is the port SafeLine listens on, such as 80 or 443, and HTTPS applications need the SSL option checked. Upstream is the real address of the original app, which is where traffic goes after inspection. After those fields are saved, the domain you entered must resolve to the IP of the server running SafeLine (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). That is the documented protection path. It does not include editing a Django settings module, registering a Flask before-request hook, or installing a Python dependency.&lt;/p&gt;
&lt;p&gt;Treat any assumption about process managers as out of scope. The sources do not require gunicorn, uWSGI, or a particular application server. They require a reachable upstream address. Keep the Python process on the address you enter, and do not invent header, health-check, or load-balancing behavior that the application guide never states.&lt;/p&gt;
&lt;h3 id=&quot;host-checks&quot;&gt;Host checks&lt;/h3&gt;
&lt;p&gt;Run the host checklist before telling anyone the app is covered. The deploy guide requires Linux, x86_64 or arm64, Docker 20.10.14 or above, and Docker Compose 2.0.0 or above. The minimum environment is 1 CPU core, 1 GB of memory, and 5 GB of disk, and the CPU must support ssse3 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). An ARM server has a further constraint in that same guide: SafeLine installed on ARM requires a Pro license to work properly, and the Personal Edition is not supported on ARM. If any item fails, stop. A Python app behind an unfinished install is not a protected app.&lt;/p&gt;
&lt;h3 id=&quot;what-you-can-claim&quot;&gt;What you can claim&lt;/h3&gt;
&lt;p&gt;Map documented detections to the routes you actually expose, and leave unsupported claims off the list. The introduction lists defenses for SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abuse. Its core capabilities also include proactive bot-abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The feature list adds rate limiting for DoS attacks, brute-force attempts, traffic surges, and other abuse by throttling traffic that exceeds defined limits. It also describes a CAPTCHA challenge that allows human users while blocking crawlers and bots, an authentication challenge that blocks visitors who do not enter the password, and dynamic protection that encrypts HTML and JS on each visit (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Those controls can sit beside a Python app, but they are not framework fixes. SQL injection filtering does not replace parameterized queries. XSS filtering does not replace template escaping. Path-traversal and command-injection filtering do not replace safe file and process handling. Rate limiting can sit in front of a login route, but the supplied documents do not publish a Flask-specific or Django-specific threshold, so this checklist must not invent one. The authentication challenge is easy to misread. It is a visitor password gate, not a Django authentication backend and not a Flask session login. Dynamic protection is documented for HTML and JS, so do not assume it rewrites a JSON response body.&lt;/p&gt;
&lt;h3 id=&quot;what-stays-in-the-app&quot;&gt;What stays in the app&lt;/h3&gt;
&lt;p&gt;Keep application controls on the same checklist as proxy controls. SafeLine is described as filtering, monitoring, and blocking malicious HTTP traffic, and as preventing unauthorized data from leaving the app by applying policies that separate malicious traffic from safe traffic (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). That is a traffic decision. Authorization bugs, weak session secrets, leaked configuration, and vulnerable dependencies can still arrive as requests that look ordinary. CSRF checks, permission checks, input validation, and secret handling stay in Flask or Django after the proxy is live.&lt;/p&gt;
&lt;h3 id=&quot;proof-before-extra-controls&quot;&gt;Proof before extra controls&lt;/h3&gt;
&lt;p&gt;Verify with the cases the application guide already publishes, and only against a site you control. The examples cover SQL injection, XSS, path traversal, code injection, and XXE as query-string tests. Replace the sample domain with your own. If the blocking page appears, the guide treats that as evidence that the attack was defended. Then open the console attack list and click detail for the event (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). A checklist is incomplete if the only proof is that the homepage still loads. Legitimate traffic must pass, and a known-bad request from that documented set must be recorded.&lt;/p&gt;
&lt;p&gt;Turn on extra visitor challenges only after that path is proven. Rate limiting, CAPTCHA, the authentication challenge, and dynamic HTML and JS encryption all change what a normal user experiences. The documents describe what each control does. They do not give a Python preset or claim that one mode fits every Django admin and every Flask API. Choose the control that matches an abuse you can name: floods and brute force for rate limiting, bot traffic for CAPTCHA, a shared gate password for the authentication challenge, and HTML or JS exposure for dynamic protection. If you cannot name the abuse, leave the extra control off.&lt;/p&gt;
&lt;p&gt;The documentation states that SafeLine is production-ready (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). That statement does not add a Flask or Django connector, and it does not replace the checks above. Decide yes only when the host meets the deploy requirements, the application entry points at a real upstream, DNS lands on the SafeLine server, a documented attack is blocked and visible in the console, and the app still enforces its own authorization and input rules.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-document-a-flask-or-django-plugin-&quot;&gt;Does SafeLine document a Flask or Django plugin?&lt;/h4&gt;
&lt;p&gt;No. The install guide and the application guide describe deployment and an HTTP reverse proxy with Domain, Port, and Upstream. They do not document a Python package or framework middleware (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). Do not plan on an in-app integration that these sources never describe.&lt;/p&gt;
&lt;h4 id=&quot;where-does-the-python-process-belong-&quot;&gt;Where does the Python process belong?&lt;/h4&gt;
&lt;p&gt;It belongs at the upstream address. Clients should reach the domain that resolves to the SafeLine server. SafeLine listens on the configured port, inspects the request, and forwards safe traffic to the real application address (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). The framework can keep serving on the address you configured as upstream.&lt;/p&gt;
&lt;h4 id=&quot;can-i-drop-csrf-query-safety-or-login-checks-&quot;&gt;Can I drop CSRF, query safety, or login checks?&lt;/h4&gt;
&lt;p&gt;No. Nothing in the cited documents says a WAF replaces application authorization, template escaping, or safe query construction. Use the proxy to filter documented web attacks and abuse patterns, and keep those controls in the app (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;how-should-i-test-without-inventing-payloads-&quot;&gt;How should I test without inventing payloads?&lt;/h4&gt;
&lt;p&gt;Use the five cases published in the application guide: SQL injection, XSS, path traversal, code injection, and XXE. Point them at your own protected domain. Expect the blocking page, then confirm the event in the console attack list and open its detail (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;which-extra-control-is-not-an-application-login-&quot;&gt;Which extra control is not an application login?&lt;/h4&gt;
&lt;p&gt;The authentication challenge. When it is turned on, visitors must enter the password or they are blocked (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). That is a site gate. Flask and Django user accounts remain a separate system, and this checklist should not merge the two.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32769/how-to-decide-if-a-flask-or-django-app-needs-a-waf</guid>
<pubDate>Wed, 28 Oct 2026 02:29:47 +0000</pubDate>
</item>
<item>
<title>Thirteen PRs in one afternoon, and the boring rule that made it possible.</title>
<link>https://coderlegion.com/32701/thirteen-prs-in-one-afternoon-and-the-boring-rule-that-made-it-possible</link>
<description>&lt;p&gt;I sat down one afternoon with a list of six things I wanted to ship to JobSearch, the job hunting tool I wrote for my own job hunt and then kept running in production for exactly one user. Three small features, two pieces of tech debt, and one refactor I had been avoiding for a month because it deleted a safety net and replaced it with a promise.&lt;/p&gt;
&lt;p&gt;I expected to finish two of them. Maybe three if the afternoon went well.&lt;/p&gt;
&lt;p&gt;Six hours later thirteen pull requests were merged into &lt;code&gt;main&lt;/code&gt;, every one of them green, every one of them live. I have gone back over that day more than once since, because the interesting part is not the number. Thirteen is a number I got by accident. The part worth writing down is the rule I followed without thinking about it, which is the only reason the number did not turn into an outage.&lt;/p&gt;
&lt;h3 id=&quot;what-was-actually-on-the-table&quot;&gt;What was actually on the table&lt;/h3&gt;
&lt;p&gt;JobSearch is a FastAPI application with PostgreSQL and Redis behind it, deployed on Render. Nothing exotic. At the time it had 394 tests and a CI pipeline with nine stages: linting, formatting, a security scanner, a dependency audit, stylelint, CodeQL, the test suite, a Docker build, and the deploy. It is a real production app with a user base of one, and that one user files very detailed bug reports.&lt;/p&gt;
&lt;p&gt;I started by asking Claude to plan the work rather than do it. What came back was a six PR roadmap ordered by risk, cheapest and safest first, with the refactor I was scared of sitting last. I changed two things about the order and then we just started walking down it. That planning step is the only reason the afternoon had a shape at all. Without it I would have opened the scary one first, because that is what I always do.&lt;/p&gt;
&lt;h3 id=&quot;the-rule&quot;&gt;The rule&lt;/h3&gt;
&lt;p&gt;Every single PR went the same way, with no exceptions and no shortcuts when I got tired:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Branch from the latest &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One concern per branch. Never a bug fix riding along with a feature.&lt;/li&gt;
&lt;li&gt;Tests written for the change.&lt;/li&gt;
&lt;li&gt;Push, wait for nine green checks, merge, delete the branch.&lt;/li&gt;
&lt;li&gt;Pull &lt;code&gt;main&lt;/code&gt;, then start the next one.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Written down it looks like the first page of any guide to working with git, and I know how unremarkable it sounds. It is also the whole answer. No two branches were ever open at the same time, so no two changes ever had to be reasoned about together, so no merge ever produced a surprise. When something did go wrong, and it did, the surface area of what could have caused it was one small diff, not six overlapping ones.&lt;/p&gt;
&lt;p&gt;We never rolled anything back. Production never broke. Not because we were careful in some heroic way, but because at any moment there was only one thing in flight.&lt;/p&gt;
&lt;h3 id=&quot;who-did-what&quot;&gt;Who did what&lt;/h3&gt;
&lt;p&gt;Claude did the typing and most of the verification. I did the decisions and the order.&lt;/p&gt;
&lt;p&gt;That split sounds like a slogan, so here is what it looked like in practice. On two of the thirteen PRs I threw away the first proposal, not because the code was wrong but because it had quietly grown: I asked for a fix and got the fix plus a small refactor of the module around it plus a new helper that nothing else would ever call. Both times the second attempt was a third of the size and did exactly what I asked. Scope creep is the failure mode I watch for now. It arrives helpfully.&lt;/p&gt;
&lt;p&gt;I also read every diff before merging. All thirteen. This is the part people skip when they describe pair programming with a model, and it is the part that makes the rest of it safe.&lt;/p&gt;
&lt;h3 id=&quot;the-claim-i-stopped-believing&quot;&gt;The claim I stopped believing&lt;/h3&gt;
&lt;p&gt;Twice that afternoon Claude told me the test suite was passing, and twice it was not.&lt;/p&gt;
&lt;p&gt;Both times the mechanism was the same. A slow test had been skipped locally, the summary line said everything else was green, and the report I got was true about what had run and silent about what had not. Running &lt;code&gt;pytest&lt;/code&gt; myself took ten seconds. Believing the summary would have cost a full CI cycle each time, and worse, it would have taught me that the summary is worth believing.&lt;/p&gt;
&lt;p&gt;That is now a habit rather than a rule: when the report is about whether we are allowed to proceed, I check the report myself. Not out of suspicion. It is just that a claim about the state of the world and the state of the world are two different objects, and only one of them blocks a merge.&lt;/p&gt;
&lt;h3 id=&quot;the-one-i-was-afraid-of&quot;&gt;The one I was afraid of&lt;/h3&gt;
&lt;p&gt;The last PR on the list deleted about 250 lines of code whose only job was repairing JSON that Claude had just written, and replaced the whole thing with Anthropic's tool use and a forced schema. Net delta across the client and its tests was around 370 lines removed.&lt;/p&gt;
&lt;p&gt;It went out alone, last, behind its own flag, with nothing else in flight. And it introduced a bug that took until the next morning to notice, because the bug was not in the code. It was in a prompt rule that had been surviving on ambiguity. I wrote that one up separately, in &lt;a rel=&quot;nofollow&quot; href=&quot;https://marcobellingeri.dev/en/writing/tool-use-jobsearch&quot;&gt;from parsing JSON in the text to Claude's tool use&lt;/a&gt;, because it deserves its own piece.&lt;/p&gt;
&lt;p&gt;The relevant bit here is that the riskiest change was the easiest one to diagnose the next day. One PR, one concern, one diff to reread. If it had shipped in the middle of the pile with four other things, I would have spent that morning bisecting instead of thinking.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-keep&quot;&gt;What I would keep&lt;/h3&gt;
&lt;p&gt;Plan before you execute, even for an afternoon. Especially for an afternoon, because that is when the temptation to just start is strongest.&lt;/p&gt;
&lt;p&gt;Keep the reviews. The model writes faster than you read, and that gap is where the mistakes live.&lt;/p&gt;
&lt;p&gt;And keep the boring rule. One branch, one concern, wait for green. It is not a productivity technique and it does not feel fast. It is what let me work for six hours at that pace without ever being in a position where I could not explain what was running in production.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32701/thirteen-prs-in-one-afternoon-and-the-boring-rule-that-made-it-possible</guid>
<pubDate>Tue, 27 Oct 2026 21:03:45 +0000</pubDate>
</item>
<item>
<title>The Third Bombshell of the Night: I Wrote a Multi-Threaded ARP Network Scanner and Port Analyzer Usi</title>
<link>https://coderlegion.com/32743/the-third-bombshell-the-night-wrote-multi-threaded-arp-network-scanner-and-port-analyzer</link>
<description>&lt;p&gt;Well, Coder Legion community, sleep has completely slipped my mind tonight! After working on Duino-Coin optimization and a trigonometric roulette physics engine, I’ve now switched into &quot;cybersecurity mode.&quot;&lt;br&gt;
Like any &quot;delightfully eccentric&quot; programmer curious about what’s happening on their local network—using nothing but an old, trusty computer with an i5-4200U processor and 4GB of RAM—I’ve coded my own Aggressive ARP Network Scanner using Python’s &lt;code&gt;scapy&lt;/code&gt; and &lt;code&gt;socket&lt;/code&gt; libraries.&lt;br&gt;
What does this script do?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Autonomous Device Detection: It stealthily fires ARP requests across the local network, sniffing out active devices and their MAC addresses within the subnet mask.&lt;/li&gt;
&lt;li&gt;Multi-Threaded Rapid Port Scanning: It employs a multi-threaded scanning mechanism to uncover open ports on the detected devices—delivering lightning-fast results without freezing up my aging machine.&lt;br&gt;
In the middle of the night, in my excitement, I even uploaded the code to GitHub twice—once with a Turkish character in the filename (&lt;code&gt;ağ.py&lt;/code&gt;) and once without (&lt;code&gt;ag.py&lt;/code&gt;)! (The &quot;hacker&quot; adrenaline—forgive me!) I’ve sent so much packet traffic through my home router that it’s probably crying right now, as if a cybercrime unit just raided the place.&lt;br&gt;
I feel like an elite cybersecurity expert, hacking the world from a dark room. You can find the code on my GitHub profile (&lt;code&gt;kozmonot20&lt;/code&gt;). Let’s just hope I don’t end up blowing up my smart TV or the router!&lt;br&gt;
73! (Best regards, Cyber Crab)&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32743/the-third-bombshell-the-night-wrote-multi-threaded-arp-network-scanner-and-port-analyzer</guid>
<pubDate>Tue, 27 Oct 2026 20:58:23 +0000</pubDate>
</item>
<item>
<title>I’m back, and this time we’re coming out of the casino, not the mine! 😎</title>
<link>https://coderlegion.com/32740/im-back-and-this-time-were-coming-out-of-the-casino-not-the-mine</link>
<description>&lt;p&gt;This is just the initial version; rest assured, I will continue to improve it. You can find my code on GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kozmonot20/rulet&quot;&gt;https://github.com/kozmonot20/rulet&lt;/a&gt;. Now, getting to the real question: do you think this is illegal? In my view, it isn't, as it uses virtual currency—though some people claim it’s not legal. What do you think? Please share your thoughts in the comments.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32740/im-back-and-this-time-were-coming-out-of-the-casino-not-the-mine</guid>
<pubDate>Tue, 27 Oct 2026 20:36:42 +0000</pubDate>
</item>
<item>
<title>Here I am again, this time with a buddy of mine who’s a Monero miner.</title>
<link>https://coderlegion.com/32739/here-i-am-again-this-time-with-a-buddy-of-mine-whos-a-monero-miner</link>
<description>&lt;p&gt;Yes, I’m back—and this time, we’re shattering all limits. Here is a C++ integrated Monero miner that won’t melt your processor:&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kozmonot20/tam-monorec/blob/main/moneroc.py&quot;&gt;enter link description here&lt;/a&gt;&lt;br&gt;
import os&lt;br&gt;
import sys&lt;br&gt;
import subprocess&lt;br&gt;
import urllib.request&lt;br&gt;
import zipfile&lt;br&gt;
import json&lt;br&gt;
import time&lt;/p&gt;
&lt;h2 id=&quot;----kozmo-titan-high-speed-settings----&quot;&gt;--- KOZMO TITAN HIGH-SPEED SETTINGS ---&lt;/h2&gt;
&lt;p&gt;MONERO_CUZDAN = &quot;you&quot;&lt;br&gt;
POOL_ADRESI = &quot;gulf.moneroocean.stream:10128&quot; &lt;br&gt;
MINER_NAME = &quot;Kozmo_Titan_C_V5&quot;&lt;/p&gt;
&lt;h2 id=&quot;g-venli-ve-stabil-al-an-resmi-xmrig-c-derlemesi&quot;&gt;Güvenli ve stabil çalışan resmi XMRig C++ derlemesi&lt;/h2&gt;
&lt;p&gt;XMRIG_URL = &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com&quot;&gt;https://github.com&lt;/a&gt;&quot;&lt;br&gt;
ZIP_NAME = &quot;xmrig.zip&quot;&lt;br&gt;
FOLDER_NAME = &quot;xmrig-6.21.0&quot;&lt;/p&gt;
&lt;p&gt;def altyapi_kontrol():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;C++ motorunun varlığını kontrol eder, yoksa temizce kurar.&quot;&quot;&quot;
# Eğer bozuk klasör kalıntıları varsa temizliyoruz
if os.path.exists(FOLDER_NAME) and not os.path.exists(os.path.join(FOLDER_NAME, &quot;xmrig.exe&quot;)):
    print(&quot;🗑️  Bozuk klasör kalıntıları temizleniyor...&quot;)
    try: os.rmdir(FOLDER_NAME)
    except: pass

if not os.path.exists(FOLDER_NAME):
    print(&quot;🛰️  Yüksek Performanslı C++ Motoru İndiriliyor...&quot;)
    if os.path.exists(ZIP_NAME):
        os.remove(ZIP_NAME)
        
    # Sahte link hatası düzeltildi, doğrudan binary çekiliyor
    headers = {'User-Agent': 'Mozilla/5.0'}
    req = urllib.request.Request(XMRIG_URL, headers=headers)
    with urllib.request.urlopen(req) as response, open(ZIP_NAME, 'wb') as out_file:
        out_file.write(response.read())
        
    print(&quot;📦 Paket içeriği C++ olarak dışarı çıkartılıyor...&quot;)
    with zipfile.ZipFile(ZIP_NAME, 'r') as zip_ref:
        zip_ref.extractall(&quot;.&quot;)
        
print(&quot;🟢 C++ Altyapısı stabil hale getirildi.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def profesyonel_config_enjekte_et():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Donanımı yormadan maksimum hızı veren Kozmo JSON ayarı.&quot;&quot;&quot;
config = {
    &quot;autosave&quot;: True,
    &quot;cpu&quot;: {
        &quot;enabled&quot;: True,
        &quot;max-threads-hint&quot;: 60, # 🧊 %35 yük sınırı: Fanlar çıldırmaz ama Python'dan 50 kat daha hızlıdır!
        &quot;perf-mode&quot;: 1
    },
    &quot;pools&quot;: [
        {
            &quot;url&quot;: POOL_ADRESI,
            &quot;user&quot;: MONERO_CUZDAN,
            &quot;pass&quot;: MINER_NAME,
            &quot;keepalive&quot;: True,
            &quot;tls&quot;: False
        }
    ]
}

# Klasör kontrol mekanizması
if not os.path.exists(FOLDER_NAME):
    os.makedirs(FOLDER_NAME)
    
config_path = os.path.join(FOLDER_NAME, &quot;config.json&quot;)
with open(config_path, &quot;w&quot;) as f:
    json.dump(config, f, indent=4)
print(&quot;⚙️  Hafif C++ konfigürasyonu enjekte edildi.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def yuksek_hizli_kaziyi_baslat():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;C++ motorunu arka planda otonom olarak ateşler.&quot;&quot;&quot;
print(f&quot;\n🪐 Kozmonot20 Titan Kripto Laboratuvarı v5.0 [HIGH-SPEED]&quot;)
print(f&quot;💰 Hedef: Gerçek Monero (XMR) | Cüzdan: Coin Wallet (Space)&quot;)
print(f&quot;⚡ Mod: Dengeli Güç (Python hızı x50 Kat Artırıldı)\n&quot;)

xmrig_exe = os.path.join(FOLDER_NAME, &quot;xmrig.exe&quot;)

if not os.path.exists(xmrig_exe):
    print(&quot;❌ HATA: xmrig.exe bulunamadı! Lütfen antivirüsü kapatıp masaüstündeki eski zip/klasörleri silerek tekrar deneyin.&quot;)
    return
    
# Motoru arka planda tamamen kısıtlanmış parametrelerle başlatıyoruz
process = subprocess.Popen([xmrig_exe], cwd=FOLDER_NAME)

try:
    while True:
        time.sleep(0.5)
except KeyboardInterrupt:
    print(&quot;\n🛑 Ctrl+C Algılandı! Madenci güvenli bir şekilde kapatılıyor...&quot;)
    process.terminate()
    print(&quot;🧊 İşlemci serbest bırakıldı. İyi sabahlar Kozmo!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == &quot;&lt;strong&gt;main&lt;/strong&gt;&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    altyapi_kontrol()
    profesyonel_config_enjekte_et()
    yuksek_hizli_kaziyi_baslat()
except Exception as e:
    print(f&quot;❌ Kritik Hata: {e}&quot;)
    print(&quot;💡 Çözüm: Masaüstündeki 'xmrig.zip' ve 'xmrig-6.21.0' klasörünü tamamen silip kodu yeniden başlatın.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope no one gets blown up (that’s my current plan; rest assured, in the future, Elon Musk will come to me to be an apprentice).&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32739/here-i-am-again-this-time-with-a-buddy-of-mine-whos-a-monero-miner</guid>
<pubDate>Tue, 27 Oct 2026 20:19:55 +0000</pubDate>
</item>
<item>
<title>Why the LLM in my automation agent never clicks anything</title>
<link>https://coderlegion.com/32729/why-the-llm-in-my-automation-agent-never-clicks-anything</link>
<description>&lt;p&gt;I'm a computer engineering student in Manaus, Brazil, and for my research project I've been building &lt;strong&gt;ANCHOR&lt;/strong&gt;, a web automation agent: you describe a task in plain language (&quot;Register Maria Silva, IT department, contractor, accept the terms and save&quot;), and it runs the task in the browser. Everything runs locally, with small models (Qwen 3.5, 4B and 9B, through Ollama) on a GTX 1050 Ti with 4 GB.&lt;/p&gt;
&lt;p&gt;This post is about the one design decision that made the biggest difference, and a few things I learned by measuring everything.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-1&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;Classic RPA depends on fixed selectors written by a developer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;page.locator('[data-test=&quot;add-to-cart&quot;]').click()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works until the page changes, and every automation needs someone who codes, far from the person who actually knows the task.&lt;/p&gt;
&lt;p&gt;The trendy answer is to put an LLM in control: it looks at the page and decides where to click. With small models, that has a serious problem, which I measured (below): the model &lt;strong&gt;says it's done when it isn't&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-decision-the-model-only-plans&quot;&gt;The decision: the model only plans&lt;/h3&gt;
&lt;p&gt;In ANCHOR, the model &lt;strong&gt;never clicks anything&lt;/strong&gt;. It gets the request and a summary of the page (the elements, numbered), and returns a plan: goals and steps, using the elements' names (&quot;fill Full name&quot;, &quot;check Contractor&quot;, &quot;click Save registration&quot;).&lt;/p&gt;
&lt;p&gt;A deterministic engine, with no AI, carries it out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A heuristic finds each element&lt;/strong&gt; from the step's description, combining text, label, role and context (synonyms, nearby text), and &lt;strong&gt;refuses when it isn't sure&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When it refuses, the user decides&lt;/strong&gt;: the candidates are numbered on the page, and you pick one (or click the right element).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The choice is remembered&lt;/strong&gt;, so the next run doesn't ask.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Each action's effect is checked&lt;/strong&gt; (did the page change? did the field keep the value? did an error appear?), and the end of the run is checked against the request.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/LucasDantas2701/ANCHOR/main/docs/images/user_choice_remembered.gif&quot; alt=&quot;The site changed, ANCHOR asks, remembers, and the next run asks nothing&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the GIF, the site changed after the automation was saved (&quot;Add to cart&quot; became &quot;Add to bag&quot;). Instead of guessing, ANCHOR asks which button to use, remembers the answer, and the next run goes on its own.&lt;/p&gt;
&lt;h3 id=&quot;learn-once-replay-without-the-model&quot;&gt;Learn once, replay without the model&lt;/h3&gt;
&lt;p&gt;A task that worked can be saved. The first run plans with the model (about 70 seconds on my GPU); the next ones replay the approved plan &lt;strong&gt;without calling the model&lt;/strong&gt;, in about 3 seconds, even once per row of a spreadsheet. When the site changes and a saved step breaks, the automation recovers and &lt;strong&gt;records what it changed&lt;/strong&gt;, and that record can be reviewed and undone.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/LucasDantas2701/ANCHOR/main/docs/images/saved_automation_spreadsheet.gif&quot; alt=&quot;A saved automation replaying a registration once per spreadsheet row, with no model calls&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-the-measurements-showed&quot;&gt;What the measurements showed&lt;/h3&gt;
&lt;p&gt;I built a resilience benchmark: 30 tasks on pages altered in 5 levels (renamed IDs, synonyms, restructured layouts, cookie banners and hidden sections, lookalike decoys with injected instructions), 150 runs per executor and model, with no user to help:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Executor &lt;/th&gt;&lt;th&gt;Tasks done &lt;/th&gt;&lt;th&gt;False successes&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Fixed-selector script (classic RPA) &lt;/td&gt;&lt;td&gt;53% &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LLM in control (4B / 9B) &lt;/td&gt;&lt;td&gt;68% / 72% &lt;/td&gt;&lt;td&gt;33 / 41&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ANCHOR (4B / 9B) &lt;/td&gt;&lt;td&gt;82% / 84% &lt;/td&gt;&lt;td&gt;2 / 1&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The column that surprised me most was &lt;strong&gt;false successes&lt;/strong&gt;: the LLM-in-control agent claimed it had done the task, without doing it, in about one run in four. For whoever relies on the automation, that's worse than failing, because nobody finds out.&lt;/p&gt;
&lt;p&gt;ANCHOR's weak spot is pages with part of the content hidden behind a &quot;Show more&quot; button (47%, vs 57–60% for the LLM in control). And an honest caveat: these are development numbers, where I tuned the system looking at the tasks. For 1.0, there's a closed set of 80 requests written by colleagues who never saw the project, which will run only once, at the end.&lt;/p&gt;
&lt;h3 id=&quot;three-lessons&quot;&gt;Three lessons&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Re-measure everything after every change.&lt;/strong&gt; The first full benchmark run revealed four bugs no test had caught (for example, the barrier against destructive actions let a &quot;Delete all&quot; through when the request was &quot;delete Bruno Lima&quot;). And two of my first fixes created new problems, which only showed up because I re-ran the other evaluations too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A prompt change affects tasks that have nothing to do with it.&lt;/strong&gt; When I added table extraction, I put two lines in the prompt explaining the new actions. The 4B model started stopping a search after filling the box, and the 9B one left the contract type out of a registration. The fix was to show those lines only when the request is about reading data; for every other request, the prompt is identical, byte for byte, to the previous version.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Never put your password in a request to an AI.&lt;/strong&gt; Testing on LinkedIn, I wrote my e-mail and password in the request, and it typed them. The password never left my computer, but it went through the model, which shouldn't happen. That became version 0.3.1: ANCHOR never types a password, refuses a request with a password before it reaches the model, and when a task needs a login, it stops and waits for you to log in by hand (the session is kept for the next runs).&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;The project is on GitHub, with the setup steps in the README:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/LucasDantas2701/ANCHOR&quot;&gt;https://github.com/LucasDantas2701/ANCHOR&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Testers and opinions are very welcome, especially failure cases on real sites: that's the most useful feedback. The next version (0.4) adds file downloads and uploads, new tabs and iframes.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Disclosure: the code was written with help from an AI assistant (Claude), which also helped me write this post; the project, the decisions and the evaluation are mine. The code is source-available (PolyForm Strict): you can read and use it, but not modify or redistribute it.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32729/why-the-llm-in-my-automation-agent-never-clicks-anything</guid>
<pubDate>Tue, 27 Oct 2026 16:57:45 +0000</pubDate>
</item>
<item>
<title>From Lab to Code #2: Exploring Spatial Audio with Python and SciPy</title>
<link>https://coderlegion.com/32674/from-lab-to-code-2-exploring-spatial-audio-with-python-and-scipy</link>
<description>&lt;p&gt;Most of us can recognize where a sound is coming from without consciously thinking about it.&lt;/p&gt;
&lt;p&gt;If someone speaks to our left, we naturally turn in that direction. If a car passes behind us, we can often estimate its movement without seeing it.&lt;/p&gt;
&lt;p&gt;But how does our auditory system determine where a sound is located?&lt;/p&gt;
&lt;p&gt;Interestingly, much of this ability depends on small differences between the signals arriving at our left and right ears.&lt;/p&gt;
&lt;p&gt;A sound may reach one ear slightly earlier, or with a different intensity, than the other.&lt;/p&gt;
&lt;p&gt;These differences can be analyzed using signal processing techniques such as &lt;strong&gt;cross-correlation, convolution, and impulse response analysis&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For the second project in my &lt;strong&gt;From Lab to Code&lt;/strong&gt; series, I wanted to explore these concepts through an interactive Python application.&lt;/p&gt;
&lt;p&gt;The result is &lt;strong&gt;Spatial Audio Playground&lt;/strong&gt;: a small experimental application that lets users position virtual sound sources, compare mono and binaural audio, and visualize some of the signals responsible for directional hearing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 02 — Spatial Audio Playground:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-are-we-building-&quot;&gt;What Are We Building?&lt;/h3&gt;
&lt;p&gt;The application explores four main ideas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cross-correlation&lt;/strong&gt; to measure the relative delay between signals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interaural Time Difference (ITD)&lt;/strong&gt; and &lt;strong&gt;Interaural Level Difference (ILD)&lt;/strong&gt; to understand directional hearing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binaural synthesis&lt;/strong&gt; to generate separate audio signals for the left and right ears.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive spatial audio&lt;/strong&gt; using Python, SciPy, Streamlit, and a small JavaScript-based soundstage.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Users can move two virtual sound sources around a listener, adjust their positions and gains, and compare the resulting audio through headphones.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Ertugrulmutlu/from-lab-to-code/main/02-spatial-audio/results/stage-preview.png&quot; alt=&quot;Spatial Audio Playground interface&quot;&gt;&lt;/p&gt;
&lt;p&gt;The project uses procedurally generated audio and educational impulse-response models by default.&lt;/p&gt;
&lt;p&gt;No pretrained models, external audio datasets, or API calls are required.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-cross-correlation-detecting-delays-between-signals&quot;&gt;1. Cross-Correlation: Detecting Delays Between Signals&lt;/h3&gt;
&lt;p&gt;Before exploring spatial audio, we need a way to measure the relative timing of two signals.&lt;/p&gt;
&lt;p&gt;Consider a simple example.&lt;/p&gt;
&lt;p&gt;We have two discrete signals containing impulses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first signal has an impulse at sample index 5.&lt;/li&gt;
&lt;li&gt;The second signal has impulses at indices 9 and 14, with amplitudes of 1.0 and 0.5.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The corresponding differences in sample positions are +4 and +9.&lt;/p&gt;
&lt;p&gt;How can we detect these shifts automatically?&lt;/p&gt;
&lt;h4 id=&quot;understanding-cross-correlation&quot;&gt;Understanding Cross-Correlation&lt;/h4&gt;
&lt;p&gt;Cross-correlation measures similarity between two signals as one is shifted relative to the other.&lt;/p&gt;
&lt;p&gt;For discrete signals, one convention is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;R_yx[k] = Σ y[n] · conjugate(x[n-k])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The resulting function tells us how strongly the signals align at different sample lags.&lt;/p&gt;
&lt;p&gt;For simple impulse signals, alignment produces distinct correlation peaks.&lt;/p&gt;
&lt;h4 id=&quot;implementing-cross-correlation-with-scipy&quot;&gt;Implementing Cross-Correlation with SciPy&lt;/h4&gt;
&lt;p&gt;First, generate the signals:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
from scipy import signal

num_samples = 20

reference = signal.unit_impulse(
    num_samples, idx=5
)

shifted = (
    signal.unit_impulse(num_samples, idx=9)
    + 0.5 * signal.unit_impulse(
        num_samples, idx=14
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, calculate the correlation and its corresponding lags:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;correlation = signal.correlate(
    shifted,
    reference,
    mode=&quot;full&quot;
)

lags = signal.correlation_lags(
    len(shifted),
    len(reference),
    mode=&quot;full&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The correlation contains two nonzero peaks:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Lag &lt;/th&gt;&lt;th&gt;Correlation amplitude&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;+4 &lt;/td&gt;&lt;td&gt;1.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;+9 &lt;/td&gt;&lt;td&gt;0.5&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The first peak is larger because the corresponding impulse has a higher amplitude.&lt;/p&gt;
&lt;p&gt;An important detail is the &lt;strong&gt;order of the arguments&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this example, &lt;code&gt;correlate(shifted, reference)&lt;/code&gt; produces positive lags for impulses occurring later in the shifted signal.&lt;/p&gt;
&lt;p&gt;Reversing the arguments changes the lag sign convention.&lt;/p&gt;
&lt;h4 id=&quot;from-sample-lag-to-time-delay&quot;&gt;From Sample Lag to Time Delay&lt;/h4&gt;
&lt;p&gt;A sample shift can also be expressed as a time difference.&lt;/p&gt;
&lt;p&gt;If the sampling frequency is &lt;code&gt;fs&lt;/code&gt;, then:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;delay = lag / fs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For example, at a sampling frequency of 44,100 Hz, a delay of four samples corresponds to approximately 0.091 milliseconds.&lt;/p&gt;
&lt;p&gt;This becomes important when comparing the arrival times of audio signals at our two ears.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Cross-correlation gives us a practical way to estimate relative timing differences, provided the signals contain sufficiently identifiable patterns.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-how-do-we-hear-direction-itd-and-ild&quot;&gt;2. How Do We Hear Direction? ITD and ILD&lt;/h3&gt;
&lt;p&gt;Our ears receive two slightly different versions of the same sound.&lt;/p&gt;
&lt;p&gt;The differences between them provide important information about the location of a sound source.&lt;/p&gt;
&lt;p&gt;Two particularly useful cues are:&lt;/p&gt;
&lt;h4 id=&quot;interaural-time-difference-itd-&quot;&gt;Interaural Time Difference (ITD)&lt;/h4&gt;
&lt;p&gt;Imagine a sound source located to your left.&lt;/p&gt;
&lt;p&gt;The sound generally reaches your left ear before your right ear because the path to the right ear is longer.&lt;/p&gt;
&lt;p&gt;This arrival-time difference is called the &lt;strong&gt;Interaural Time Difference&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the application, I define:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ITD = t_right - t_left&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Positive ITD: the sound arrives at the left ear first.&lt;/li&gt;
&lt;li&gt;Negative ITD: the sound arrives at the right ear first.&lt;/li&gt;
&lt;li&gt;ITD near zero: both ears have approximately equal arrival times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To estimate ITD, the project applies a low-pass filter to the left and right impulse responses before calculating their cross-correlation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;sos = signal.butter(
    4,
    1500,
    btype=&quot;lowpass&quot;,
    fs=44100,
    output=&quot;sos&quot;
)

low_left = signal.sosfilt(
    sos, hrir[0]
)

low_right = signal.sosfilt(
    sos, hrir[1]
)

lags = signal.correlation_lags(
    len(low_right),
    len(low_left),
    mode=&quot;full&quot;
)

correlation = signal.correlate(
    low_right,
    low_left,
    mode=&quot;full&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation restricts the lag search to a physically relevant window of ±1 millisecond.&lt;/p&gt;
&lt;p&gt;The lag corresponding to the highest correlation value within this window provides an estimate of the timing difference.&lt;/p&gt;
&lt;h4 id=&quot;interaural-level-difference-ild-&quot;&gt;Interaural Level Difference (ILD)&lt;/h4&gt;
&lt;p&gt;Time is not the only difference between our ears.&lt;/p&gt;
&lt;p&gt;The head also affects how much sound energy reaches each ear.&lt;/p&gt;
&lt;p&gt;For some directions, the head partially blocks the sound travelling toward the opposite ear.&lt;/p&gt;
&lt;p&gt;This effect is especially important at higher frequencies.&lt;/p&gt;
&lt;p&gt;The resulting difference in sound level is called the &lt;strong&gt;Interaural Level Difference&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the project, a broadband ILD estimate is calculated using the energy of the two impulse responses:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ILD = 10 · log10(E_right / E_left)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;where:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;E = Σ h[n]²&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In Python:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;energy_left = np.sum(hrir[0] ** 2)
energy_right = np.sum(hrir[1] ** 2)

ild_db = 10 * np.log10(
    energy_right / energy_left
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A negative ILD means the right channel contains less impulse-response energy than the left channel.&lt;/p&gt;
&lt;p&gt;This is a broadband estimate. In real acoustic systems, ILD depends strongly on frequency, so frequency-band analysis can provide more detailed information.&lt;/p&gt;
&lt;h4 id=&quot;why-both-cues-matter&quot;&gt;Why Both Cues Matter&lt;/h4&gt;
&lt;p&gt;ITD and ILD complement each other.&lt;/p&gt;
&lt;p&gt;ITD is particularly useful for understanding low-frequency directional cues, while ILD becomes especially relevant at higher frequencies.&lt;/p&gt;
&lt;p&gt;However, these two values alone cannot completely explain spatial hearing.&lt;/p&gt;
&lt;p&gt;They do not reliably resolve all front/back or elevation ambiguities.&lt;/p&gt;
&lt;p&gt;For that, we need to consider how the head, torso, and outer ears modify the sound spectrum.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Spatial hearing depends on both when sound reaches each ear and how its level and spectrum change along the way.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-head-related-impulse-responses-modeling-the-path-to-our-ears&quot;&gt;3. Head-Related Impulse Responses: Modeling the Path to Our Ears&lt;/h3&gt;
&lt;p&gt;To simulate the sound arriving at each ear, we need to model how the human body modifies incoming audio.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;Head-Related Transfer Functions (HRTFs)&lt;/strong&gt; and &lt;strong&gt;Head-Related Impulse Responses (HRIRs)&lt;/strong&gt; become useful.&lt;/p&gt;
&lt;p&gt;An HRTF describes the directional filtering of sound in the frequency domain.&lt;/p&gt;
&lt;p&gt;An HRIR represents the corresponding impulse response in the time domain.&lt;/p&gt;
&lt;p&gt;For each sound source direction, we need two impulse responses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h_left[n]&lt;/code&gt; for the left ear.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;h_right[n]&lt;/code&gt; for the right ear.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These responses describe how an incoming signal is transformed on its way to the two ears.&lt;/p&gt;
&lt;h4 id=&quot;generating-educational-hrirs&quot;&gt;Generating Educational HRIRs&lt;/h4&gt;
&lt;p&gt;For this project, I implemented a small synthetic HRIR generator.&lt;/p&gt;
&lt;p&gt;Instead of relying on a measured dataset, the default model approximates three effects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Arrival-time differences between the ears.&lt;/li&gt;
&lt;li&gt;Head-shadow attenuation.&lt;/li&gt;
&lt;li&gt;Mild spectral coloration related to elevation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The lateral direction is calculated using the source angles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;lateral = (
    np.sin(np.deg2rad(azimuth))
    * np.cos(np.deg2rad(elevation))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application uses the following coordinate convention:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0°&lt;/code&gt;: front&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+90°&lt;/code&gt;: left&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-90°&lt;/code&gt;: right&lt;/li&gt;
&lt;li&gt;&lt;code&gt;±180°&lt;/code&gt;: behind&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The synthetic model then converts this lateral position into an approximate sample delay.&lt;/p&gt;
&lt;p&gt;It also reduces the far-ear amplitude to illustrate head-shadow effects.&lt;/p&gt;
&lt;p&gt;This provides a controllable environment for investigating spatial cues without requiring external recordings.&lt;/p&gt;
&lt;h4 id=&quot;an-important-limitation&quot;&gt;An Important Limitation&lt;/h4&gt;
&lt;p&gt;The synthetic HRIR model is intentionally simplified.&lt;/p&gt;
&lt;p&gt;Real HRIRs contain much more detailed information about reflections, diffraction, and resonances caused by an individual's anatomy.&lt;/p&gt;
&lt;p&gt;Therefore, the built-in model is most useful for understanding basic left/right directional cues.&lt;/p&gt;
&lt;p&gt;It should not be treated as a physically accurate or personalized 3D audio renderer.&lt;/p&gt;
&lt;p&gt;For more realistic experiments, the application can optionally load compatible measured HRIR data stored in the &lt;strong&gt;SOFA (Spatially Oriented Format for Acoustics)&lt;/strong&gt; format.&lt;/p&gt;
&lt;p&gt;The current implementation uses the nearest available measurement direction rather than interpolating between measurements.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-binaural-synthesis-turning-mono-into-directional-stereo&quot;&gt;4. Binaural Synthesis: Turning Mono into Directional Stereo&lt;/h3&gt;
&lt;p&gt;Now we have the essential components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A mono audio signal.&lt;/li&gt;
&lt;li&gt;A left-ear impulse response.&lt;/li&gt;
&lt;li&gt;A right-ear impulse response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next step is to combine them.&lt;/p&gt;
&lt;p&gt;This is done using &lt;strong&gt;convolution&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For a mono input signal &lt;code&gt;x[n]&lt;/code&gt;, we calculate:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;y_left[n] = x[n] * h_left[n]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;y_right[n] = x[n] * h_right[n]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here, &lt;code&gt;*&lt;/code&gt; represents the convolution operation.&lt;/p&gt;
&lt;p&gt;Each output contains a filtered version of the original sound, corresponding to a different ear.&lt;/p&gt;
&lt;h4 id=&quot;implementing-convolution-with-scipy&quot;&gt;Implementing Convolution with SciPy&lt;/h4&gt;
&lt;p&gt;The actual project uses &lt;code&gt;scipy.signal.oaconvolve&lt;/code&gt;, which implements overlap-add convolution.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from scipy import signal
import numpy as np

def render_one(mono, hrir, gain_db=0.0):
    channels = [
        signal.oaconvolve(
            mono,
            hrir[ear],
            mode=&quot;full&quot;
        )
        for ear in range(2)
    ]

    stereo = np.column_stack(channels)

    return stereo * (
        10.0 ** (gain_db / 20.0)
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is a stereo array containing separate left and right waveforms.&lt;/p&gt;
&lt;p&gt;When these signals are played through headphones, the listener receives different directional information at each ear.&lt;/p&gt;
&lt;h4 id=&quot;combining-multiple-sound-sources&quot;&gt;Combining Multiple Sound Sources&lt;/h4&gt;
&lt;p&gt;The application supports up to two independent sound sources.&lt;/p&gt;
&lt;p&gt;Each source can have its own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Audio recording.&lt;/li&gt;
&lt;li&gt;Azimuth and elevation.&lt;/li&gt;
&lt;li&gt;Gain.&lt;/li&gt;
&lt;li&gt;HRIR pair.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every mono source is processed separately.&lt;/p&gt;
&lt;p&gt;The resulting stereo signals are then summed to create a binaural mix.&lt;/p&gt;
&lt;p&gt;To make the mono/binaural comparison more consistent, the application applies one shared clipping-prevention gain to both outputs when necessary.&lt;/p&gt;
&lt;p&gt;This avoids independently peak-normalizing the two versions, which could introduce an unnecessary gain difference.&lt;/p&gt;
&lt;p&gt;It is important to note that a common gain does not guarantee equal perceived loudness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Convolution lets us transform ordinary mono audio into two direction-dependent ear signals using impulse responses.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-building-the-interactive-spatial-audio-playground&quot;&gt;5. Building the Interactive Spatial Audio Playground&lt;/h3&gt;
&lt;p&gt;Understanding the equations is useful, but I also wanted a way to experiment with them directly.&lt;/p&gt;
&lt;p&gt;That became the main motivation behind the application's interface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Ertugrulmutlu/from-lab-to-code/main/02-spatial-audio/results/overview.png&quot; alt=&quot;Spatial audio and correlation results&quot;&gt;&lt;/p&gt;
&lt;p&gt;The interface is built using &lt;strong&gt;Streamlit&lt;/strong&gt;, with an additional HTML, CSS, SVG, and JavaScript component for the interactive soundstage.&lt;/p&gt;
&lt;h4 id=&quot;moving-virtual-sound-sources&quot;&gt;Moving Virtual Sound Sources&lt;/h4&gt;
&lt;p&gt;Instead of manually entering every angle, users can drag two sound-source markers around a listener.&lt;/p&gt;
&lt;p&gt;Each marker represents an independent virtual audio source.&lt;/p&gt;
&lt;p&gt;When a source is moved, its new azimuth is sent to the Python application.&lt;/p&gt;
&lt;p&gt;The application selects or generates the corresponding impulse responses and renders new audio clips.&lt;/p&gt;
&lt;p&gt;The soundstage also supports keyboard interaction and directional presets.&lt;/p&gt;
&lt;p&gt;Importantly, this is &lt;strong&gt;offline rendering&lt;/strong&gt;, not continuous real-time spatial audio.&lt;/p&gt;
&lt;p&gt;Moving the markers changes the next generated clip rather than streaming a moving sound source.&lt;/p&gt;
&lt;h4 id=&quot;comparing-mono-and-binaural-playback&quot;&gt;Comparing Mono and Binaural Playback&lt;/h4&gt;
&lt;p&gt;One of the central features is a direct listening comparison.&lt;/p&gt;
&lt;p&gt;The application produces two outputs from the same input sources:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Centered mono:&lt;/strong&gt; A reference mix without separate left/right spatial filtering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Binaural stereo:&lt;/strong&gt; A direction-dependent mix created using individual impulse responses for each ear.&lt;/p&gt;
&lt;p&gt;Users can listen to both versions and download the binaural WAV output.&lt;/p&gt;
&lt;p&gt;Headphones are recommended because they deliver the two channels separately to the listener's ears.&lt;/p&gt;
&lt;h4 id=&quot;inspecting-the-signals&quot;&gt;Inspecting the Signals&lt;/h4&gt;
&lt;p&gt;The interface also displays:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Estimated ITD in milliseconds.&lt;/li&gt;
&lt;li&gt;Estimated ILD in decibels.&lt;/li&gt;
&lt;li&gt;Left and right HRIR waveforms.&lt;/li&gt;
&lt;li&gt;The beginning of the rendered stereo waveform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This allows users to connect the perceptual experience with the underlying signal-processing operations.&lt;/p&gt;
&lt;p&gt;The application also includes a separate cross-correlation experiment and a short theory section.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;running-the-project-locally&quot;&gt;Running the Project Locally&lt;/h3&gt;
&lt;p&gt;The complete implementation is available on GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You will need Python 3.11–3.13.&lt;/p&gt;
&lt;p&gt;Clone the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Ertugrulmutlu/from-lab-to-code.git
cd from-lab-to-code/02-spatial-audio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Launch the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application opens in your browser, typically at &lt;code&gt;http://localhost:8501&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can immediately experiment with the built-in audio examples.&lt;/p&gt;
&lt;p&gt;The project also supports uploading your own short WAV recordings and compatible SOFA datasets.&lt;/p&gt;
&lt;p&gt;To execute the automated tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m unittest discover -s tests -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The project includes 18 automated unit and integration tests covering signal processing, audio rendering, and related functionality.&lt;/p&gt;
&lt;p&gt;The main implementation is organized into separate files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;correlation.py&lt;/code&gt; — cross-correlation and interaural cue estimation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binaural.py&lt;/code&gt; — convolution, HRIR loading, and WAV handling.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audio_pipeline.py&lt;/code&gt; — rendering and mixing multiple sound sources.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;synthetic.py&lt;/code&gt; — original demonstration sounds and educational HRIRs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.py&lt;/code&gt; — the interactive Streamlit application.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;frontend/index.html&lt;/code&gt; — the draggable soundstage component.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation keeps the audio algorithms independent of the user interface.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h3&gt;
&lt;p&gt;Working on this project helped me connect several ideas that initially seemed independent.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;small timing differences can carry meaningful spatial information&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Cross-correlation is not only a mathematical technique for comparing signals. It also provides a practical way to investigate the delays that contribute to directional hearing.&lt;/p&gt;
&lt;p&gt;Second, &lt;strong&gt;impulse responses provide an elegant connection between physical systems and software&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Once we represent the acoustic path to each ear as an impulse response, generating the corresponding output becomes a convolution problem.&lt;/p&gt;
&lt;p&gt;Third, &lt;strong&gt;a signal can be mathematically correct without producing a perfectly realistic perceptual result&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Our synthetic HRIR model demonstrates timing and level differences, but human spatial hearing also depends on detailed anatomy-dependent spectral cues.&lt;/p&gt;
&lt;p&gt;Finally, I learned that making an engineering concept interactive can change how it is understood.&lt;/p&gt;
&lt;p&gt;Adjusting an azimuth angle, listening to the result, and examining the corresponding waveforms makes the relationship between theory and perception much more tangible.&lt;/p&gt;
&lt;h3 id=&quot;continuing-from-lab-to-code&quot;&gt;Continuing From Lab to Code&lt;/h3&gt;
&lt;p&gt;In the first project of this series, I explored classical computer vision using filtering, Fourier analysis, thresholding, and clustering.&lt;/p&gt;
&lt;p&gt;This second project moves from image processing to audio signal processing.&lt;/p&gt;
&lt;p&gt;Both follow the same philosophy:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Take an engineering concept, build an independent implementation, make the results observable, and explain what is happening underneath.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not every engineering experiment needs to become a large software product.&lt;/p&gt;
&lt;p&gt;Sometimes, a small interactive application is enough to reveal how an interesting concept works.&lt;/p&gt;
&lt;p&gt;The goal of &lt;strong&gt;From Lab to Code&lt;/strong&gt; is to keep exploring these ideas through practical implementations.&lt;/p&gt;
&lt;p&gt;All bundled demonstration sounds in this project are generated programmatically, and no university course materials or proprietary recordings are redistributed.&lt;/p&gt;
&lt;p&gt;If you're interested in signal processing, spatial audio, or simply curious about how computers can simulate directional hearing, I hope this project gives you something interesting to experiment with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the full series:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore Spatial Audio Playground:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32674/from-lab-to-code-2-exploring-spatial-audio-with-python-and-scipy</guid>
<pubDate>Tue, 27 Oct 2026 12:00:48 +0000</pubDate>
</item>
<item>
<title>How I Built My Own Opus Clip Alternative With Claude Code</title>
<link>https://coderlegion.com/32660/how-i-built-my-own-opus-clip-alternative-with-claude-code</link>
<description>&lt;p&gt;Opus Clip, Submagic and Vidyo all work the same way: you upload a long video, the AI scores every moment for &quot;virality&quot;, and you get the clips it likes. That is fine until you want something specific. When I am clipping a podcast for a client I don't want &quot;the most viral 30 seconds&quot;. I want &quot;every time they talk about pricing&quot; or &quot;the strongest hook in the first ten minutes&quot;.&lt;/p&gt;
&lt;p&gt;So I built my own Opus Clip alternative, called Hive Clips AI, with Claude Code. You give it a long video and a plain English prompt, and it transcribes the video, uses an LLM to find the moments that match your prompt, reframes each clip to keep the speaker in shot, burns in word-by-word captions and renders vertical clips ready to post. The prompt drives the selection, not a fixed score. That is the whole difference.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://danishsofi.in/images/blog/hive-clips/home.webp&quot; alt=&quot;Hive Clips AI home screen with a video upload area and a YouTube URL field&quot;&gt;&lt;br&gt;
&lt;em&gt;The Hive Clips AI home screen. Drop in a long video or paste a YouTube link.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here is how it works, stage by stage, and what I would tell you before you try building one.&lt;/p&gt;
&lt;h3 id=&quot;what-does-an-ai-clipping-tool-actually-do-&quot;&gt;What does an AI clipping tool actually do?&lt;/h3&gt;
&lt;p&gt;Strip the marketing away and every clipping tool is the same nine-step pipeline. I built mine so each step runs on its own and saves its result, which matters later.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Stage &lt;/th&gt;&lt;th&gt;What happens &lt;/th&gt;&lt;th&gt;What I used&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1. Ingest &lt;/td&gt;&lt;td&gt;Upload a file or paste a YouTube link &lt;/td&gt;&lt;td&gt;yt-dlp, FFmpeg&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2. Transcribe &lt;/td&gt;&lt;td&gt;Speech to text with a timestamp on every word &lt;/td&gt;&lt;td&gt;Whisper API, or faster-whisper locally&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3. Select &lt;/td&gt;&lt;td&gt;Find moments that match your prompt &lt;/td&gt;&lt;td&gt;GPT-4o with structured JSON output&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4. Review &lt;/td&gt;&lt;td&gt;You pick which candidates to keep &lt;/td&gt;&lt;td&gt;Next.js UI&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;5. Configure &lt;/td&gt;&lt;td&gt;Choose aspect ratio and caption style &lt;/td&gt;&lt;td&gt;Next.js UI&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;6. Reframe &lt;/td&gt;&lt;td&gt;Track the face and move a virtual camera &lt;/td&gt;&lt;td&gt;OpenCV, PySceneDetect&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;7. Caption &lt;/td&gt;&lt;td&gt;Build animated word-level captions &lt;/td&gt;&lt;td&gt;ASS subtitles, libass&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8. Render &lt;/td&gt;&lt;td&gt;Crop, burn captions, encode &lt;/td&gt;&lt;td&gt;FFmpeg&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;9. Deliver &lt;/td&gt;&lt;td&gt;Play, download or regenerate &lt;/td&gt;&lt;td&gt;FastAPI + Next.js&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The backend is Python with a FastAPI wrapper. The frontend is Next.js 15 with TypeScript and Tailwind. There is also a command line version, which is where I proved the whole pipeline worked before touching the UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://danishsofi.in/images/blog/hive-clips/pipeline.webp&quot; alt=&quot;Diagram of the nine Hive Clips AI pipeline stages from ingest to deliver&quot;&gt;&lt;br&gt;
&lt;em&gt;The nine stages. Select and Reframe are where most of the work went.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-does-the-ai-pick-the-right-moments-&quot;&gt;How does the AI pick the right moments?&lt;/h3&gt;
&lt;p&gt;This is the part people assume is magic. It is mostly careful plumbing.&lt;/p&gt;
&lt;p&gt;A one hour podcast transcript is tens of thousands of tokens. Sending all of it in one request gives you worse picks, so the transcript is split into 9 minute windows with a 15 second overlap. The overlap is there so a great moment that sits across a window boundary doesn't get cut in half.&lt;/p&gt;
&lt;p&gt;Each window goes to GPT-4o with the creator's prompt and a strict instruction: return 2 to 5 clips, each between 20 and 90 seconds, and only clips that genuinely match the intent. The model also rates each clip's hook from 1 to 10, judged on the first three seconds, because that is what decides whether someone stops scrolling.&lt;/p&gt;
&lt;p&gt;The output is forced into a JSON schema, so I never have to parse loose text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;clips&quot;: [
    {
      &quot;start_time&quot;: 412.3,
      &quot;end_time&quot;: 468.9,
      &quot;title&quot;: &quot;Why he stopped charging hourly&quot;,
      &quot;reasoning&quot;: &quot;Direct answer about pricing with a strong opening line&quot;,
      &quot;hook_strength&quot;: 8
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After every window is done, the candidates get merged and re-ranked, and you see them in the review screen. Nothing renders until you pick. That review step is the thing I missed most in the paid tools.&lt;/p&gt;
&lt;h3 id=&quot;why-is-auto-reframing-the-hardest-part-&quot;&gt;Why is auto-reframing the hardest part?&lt;/h3&gt;
&lt;p&gt;Turning a 16:9 video into a 9:16 clip means cropping a narrow window out of a wide frame and moving that window to follow the speaker. The obvious way is to detect the face on every frame and centre the crop on it.&lt;/p&gt;
&lt;p&gt;That looks terrible. Face detection is noisy, so the crop shakes constantly, and when the video cuts to a different camera the crop slides across the cut like a drunk camera operator.&lt;/p&gt;
&lt;p&gt;The fix is to treat the crop as a virtual camera with physics, not a box that snaps to the face:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A critically damped spring.&lt;/strong&gt; The camera moves toward the face but never overshoots. I ported Unity's SmoothDamp function for this. Smooth time is 0.55 seconds, which feels calm without lagging behind.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A deadzone.&lt;/strong&gt; If the face moves less than 6% of the crop width, the camera ignores it. This alone kills most of the jitter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A speed limit.&lt;/strong&gt; Pans have a maximum speed, so a false detection can't whip the frame across the screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scene cut resets.&lt;/strong&gt; PySceneDetect finds the hard cuts. At a cut the camera drops its speed and repositions instead of carrying a pan across the edit. Between cuts it must move smoothly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Headroom.&lt;/strong&gt; The subject sits about 10% above centre, the way a real camera operator would frame a person.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The camera path becomes a crop track that FFmpeg applies during the render. Face detection itself is a basic OpenCV detector that needs no model download. I kept the smoothing separate from detection on purpose, so I can swap in a better detector later without touching the part that makes it look good.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://danishsofi.in/images/blog/hive-clips/chart.webp&quot; alt=&quot;Chart comparing noisy face detection points with the smooth virtual camera path&quot;&gt;&lt;br&gt;
&lt;em&gt;Real output from the Hive Clips smoother on noisy test data. The camera ignores small jitter and makes one calm pan when the speaker moves.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://danishsofi.in/images/blog/hive-clips/canvas.webp&quot; alt=&quot;Hive Clips AI configure screen with output canvas options 9:16, 1:1, 4:5, 16:9 and 4:3&quot;&gt;&lt;br&gt;
&lt;em&gt;Picking the output canvas. 9:16 for Reels, Shorts and TikTok, with a live preview on the right.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you build one of these, spend your time on the smoothing, not the detector. The smoothing is what makes it look professional.&lt;/p&gt;
&lt;h3 id=&quot;how-do-the-word-by-word-captions-work-&quot;&gt;How do the word-by-word captions work?&lt;/h3&gt;
&lt;p&gt;Most tutorials use SRT subtitles. SRT can't do the captions people actually want on Shorts and Reels, where each word lights up as it is spoken.&lt;/p&gt;
&lt;p&gt;I used ASS subtitles instead. ASS supports per-word timing, colours and animation, and FFmpeg burns it into the video through libass. Because the transcript has a timestamp for every word, each word can change colour exactly when it is said.&lt;/p&gt;
&lt;p&gt;The tool ships with 24 caption presets. The original 8 cover the basics: a karaoke style, a minimal style, a news bar and an outline style. Then I added 16 styles based on what is working on short-form right now, including a Hormozi-style highlight where the full line shows and the active word recolours, a one-word-at-a-time punch style, and a word reveal where words pop in and stay.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://danishsofi.in/images/blog/hive-clips/captions.webp&quot; alt=&quot;Hive Clips AI caption style presets including Bold Pop, Karaoke Classic, Hive Yellow and Hormozi Green&quot;&gt;&lt;br&gt;
&lt;em&gt;Part of the caption preset grid. Each preset is a starting point you can fine tune per clip.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-broke-while-building-it-&quot;&gt;What broke while building it?&lt;/h3&gt;
&lt;p&gt;A few honest notes, because build logs that only show the happy path aren't useful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The web app broke when the CLI didn't.&lt;/strong&gt; When I added caption overrides (custom colours and sizes per clip), the command line kept working but the web API failed. The database was missing the new column. The fix was a one-line migration, but it taught me to test both entry points every time the data model changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Saving every stage paid off.&lt;/strong&gt; Each of the nine stages writes its result and a job record to the database. So when a render fails, I re-run the render only. I don't wait for the transcription again or pay for the AI selection twice. On a long podcast that is the difference between seconds and many minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mock mode made testing free.&lt;/strong&gt; Without an OpenAI key, the tool runs with a stand-in transcript and a simple moment picker, but reframing, captions and rendering are fully real. I could test the hardest parts of the pipeline all day without spending anything on API calls. The UI labels mock mode clearly so stand-in output is never mistaken for real picks.&lt;/p&gt;
&lt;h3 id=&quot;how-did-claude-code-fit-in-&quot;&gt;How did Claude Code fit in?&lt;/h3&gt;
&lt;p&gt;I wrote a product spec first: the nine stages, the five screens, and the rules for each step. Then I built it with Claude Code one stage at a time, starting from the command line and proving each stage worked before moving on.&lt;/p&gt;
&lt;p&gt;Two habits made the biggest difference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Interfaces before infrastructure.&lt;/strong&gt; Storage, the job queue, the database and the AI provider each sit behind a simple interface. Right now it runs on SQLite, local files and a local queue. Moving to Postgres, Cloudflare R2 and a proper job queue means swapping one piece at a time, not rewriting the app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure maths in its own file.&lt;/strong&gt; The camera smoother has no video code in it at all. That made it fast to test and easy for Claude Code to reason about, which is exactly where you want precision.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you are new to building with Claude Code, my &lt;a rel=&quot;nofollow&quot; href=&quot;https://danishsofi.in/blog/how-to-use-claude-code-to-build-app-website&quot;&gt;beginner's guide to building an app with Claude Code&lt;/a&gt; covers the setup, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://danishsofi.in/blog/claude-code-pricing-free-options-2026&quot;&gt;Claude Code pricing and free options&lt;/a&gt; covers what it costs to run.&lt;/p&gt;
&lt;h3 id=&quot;what-is-next-for-the-tool-&quot;&gt;What is next for the tool?&lt;/h3&gt;
&lt;p&gt;The current build is the full pipeline plus the web UI. On the list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sharper cut handling. Right now the reposition at a cut uses the last face position from before the cut, so on a hard camera change the frame catches up a beat late. The fix is to look ahead to the first detection after the cut.&lt;/li&gt;
&lt;li&gt;Live progress updates instead of polling every 1.5 seconds&lt;/li&gt;
&lt;li&gt;Regenerating only the captions or only the reframe, instead of the whole clip&lt;/li&gt;
&lt;li&gt;Speaker detection, so two-person podcasts can get a split-screen layout&lt;/li&gt;
&lt;li&gt;Running transcription and selection fully locally, with no API costs&lt;/li&gt;
&lt;li&gt;Auto-posting finished clips to TikTok, Instagram and YouTube Shorts&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently asked questions&lt;/h3&gt;
&lt;h4 id=&quot;is-there-a-free-opus-clip-alternative-&quot;&gt;Is there a free Opus Clip alternative?&lt;/h4&gt;
&lt;p&gt;Yes, if you are willing to run it yourself. Open-source pieces like Whisper (transcription), FFmpeg (rendering) and PySceneDetect (scene cuts) are free, and that is most of a clipping tool. The paid part is usually the AI moment selection, and you can run that on a local model to bring the cost close to zero.&lt;/p&gt;
&lt;h4 id=&quot;how-does-opus-clip-choose-which-moments-to-clip-&quot;&gt;How does Opus Clip choose which moments to clip?&lt;/h4&gt;
&lt;p&gt;Opus Clip and similar tools score moments with their own virality model and give you the top picks. You can't tell them what to look for beyond basic settings. A prompt-driven tool like mine lets you describe the moment you want in plain English.&lt;/p&gt;
&lt;h4 id=&quot;can-gpt-4o-pick-good-clips-from-a-transcript-&quot;&gt;Can GPT-4o pick good clips from a transcript?&lt;/h4&gt;
&lt;p&gt;Yes, if you give it structure. Split long transcripts into overlapping windows, ask for a fixed number of clips with strict start and end times, and force a JSON schema. Then review the picks yourself before rendering. It is good at finding topics, less reliable at judging delivery, which is why the review step matters.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-hardest-part-of-building-an-ai-clipping-tool-&quot;&gt;What is the hardest part of building an AI clipping tool?&lt;/h4&gt;
&lt;p&gt;Reframing. Detecting a face is easy. Making the crop move smoothly, without jitter, without overshooting and without sliding across scene cuts, is where most of the work goes.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-a-gpu-to-run-it-&quot;&gt;Do I need a GPU to run it?&lt;/h4&gt;
&lt;p&gt;No. Local transcription uses faster-whisper, which runs on a CPU, and the face detector is lightweight. A GPU makes encoding faster, and the renderer will use one if you have it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you want a clipping pipeline like this built for your channel or agency, &lt;a rel=&quot;nofollow&quot; href=&quot;https://danishsofi.in/contact&quot;&gt;get in touch&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://danishsofi.in/blog/build-opus-clip-alternative-claude-code&quot;&gt;danishsofi.in&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32660/how-i-built-my-own-opus-clip-alternative-with-claude-code</guid>
<pubDate>Tue, 27 Oct 2026 10:03:19 +0000</pubDate>
</item>
<item>
<title>How to Build a Real Time Activity Log for Your Web App</title>
<link>https://coderlegion.com/32629/how-to-build-a-real-time-activity-log-for-your-web-app</link>
<description>&lt;p&gt;Modern web applications generate a continuous stream of user activities. Users log in, update their profiles, create records, upload files, change permissions, and perform many other actions throughout the day. In a small application, tracking these activities manually may seem manageable. However, as an application grows, understanding who did what, when an action occurred, and what changed becomes increasingly important.&lt;/p&gt;
&lt;p&gt;This is where a &lt;strong&gt;real-time activity log&lt;/strong&gt; becomes valuable.&lt;/p&gt;
&lt;p&gt;A real-time activity log records important events within an application and makes them available to users, administrators, or support teams as they happen. Instead of searching through scattered server logs or asking users to explain what happened, developers can provide a centralized timeline of meaningful actions.&lt;/p&gt;
&lt;p&gt;For example, a project management application might display the following activities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sarah created a new project.&lt;/li&gt;
&lt;li&gt;Michael updated the project deadline.&lt;/li&gt;
&lt;li&gt;David uploaded a document.&lt;/li&gt;
&lt;li&gt;Sarah invited a new team member.&lt;/li&gt;
&lt;li&gt;An administrator changed a user's permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These events help teams understand application activity, investigate problems, and collaborate more effectively.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to design and build a real-time activity log using React, Python, FastAPI, PostgreSQL, and WebSockets. We will cover database design, event recording, real-time updates, security, performance optimization, and deployment considerations.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-real-time-activity-logs&quot;&gt;1. Understanding Real-Time Activity Logs&lt;/h3&gt;
&lt;p&gt;Before writing code, it is important to understand what an activity log does and how it differs from ordinary application logging.&lt;/p&gt;
&lt;p&gt;Traditional application logs are primarily designed for developers and infrastructure teams. They record technical information such as exceptions, failed database connections, HTTP errors, and performance problems.&lt;/p&gt;
&lt;p&gt;An activity log focuses on meaningful actions performed by users or systems.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Technical log: &lt;code&gt;POST /api/projects returned status 201&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Activity log: &lt;code&gt;Sarah created the project &quot;Website Redesign&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both records may describe the same operation, but they serve different purposes.&lt;/p&gt;
&lt;h4 id=&quot;common-use-cases&quot;&gt;Common use cases&lt;/h4&gt;
&lt;p&gt;Real-time activity logs are useful in many applications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project management platforms:&lt;/strong&gt; Track task creation, assignments, comments, and deadline changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;E-commerce applications:&lt;/strong&gt; Record order updates, refunds, inventory changes, and administrative actions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CRM systems:&lt;/strong&gt; Track customer record updates, sales activities, and changes to lead ownership.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Financial dashboards:&lt;/strong&gt; Maintain auditable records of important account operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Administrative panels:&lt;/strong&gt; Show user logins, permission changes, configuration updates, and security events.&lt;/p&gt;
&lt;p&gt;A well-designed activity log should answer three fundamental questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who performed the action?&lt;/li&gt;
&lt;li&gt;What action occurred?&lt;/li&gt;
&lt;li&gt;When did the action happen?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Depending on the application, it may also record the affected resource, the previous and updated values, the source of the action, and the result.&lt;/p&gt;
&lt;h3 id=&quot;2-choosing-the-technology-stack&quot;&gt;2. Choosing the Technology Stack&lt;/h3&gt;
&lt;p&gt;Our example uses a stack that supports structured data, asynchronous APIs, and live browser updates.&lt;/p&gt;
&lt;h4 id=&quot;frontend-react&quot;&gt;Frontend: React&lt;/h4&gt;
&lt;p&gt;React renders the activity timeline and updates the interface when new events arrive. It can connect to a WebSocket endpoint and display incoming messages without requiring a complete page refresh.&lt;/p&gt;
&lt;h4 id=&quot;backend-python-and-fastapi&quot;&gt;Backend: Python and FastAPI&lt;/h4&gt;
&lt;p&gt;FastAPI handles authentication, validates incoming requests, records activities, and exposes endpoints for retrieving historical events. Its WebSocket support allows the backend to communicate with connected browsers.&lt;/p&gt;
&lt;h4 id=&quot;database-postgresql&quot;&gt;Database: PostgreSQL&lt;/h4&gt;
&lt;p&gt;PostgreSQL stores activity records permanently. It supports relational queries, indexes, transactions, and pagination, making it suitable for an activity timeline.&lt;/p&gt;
&lt;h4 id=&quot;real-time-communication-websockets&quot;&gt;Real-time communication: WebSockets&lt;/h4&gt;
&lt;p&gt;WebSockets establish a persistent connection between the browser and server. After a new activity is committed to the database, the backend can publish an event to authorized connected clients.&lt;/p&gt;
&lt;h4 id=&quot;optional-infrastructure-redis&quot;&gt;Optional infrastructure: Redis&lt;/h4&gt;
&lt;p&gt;For applications running multiple backend instances, Redis Pub/Sub or a durable message broker can distribute events between servers. Redis is not required for a single-instance prototype.&lt;/p&gt;
&lt;p&gt;The architecture looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User performs an action
          |
          v
React frontend
          |
          v
FastAPI endpoint
          |
          v
Business operation + database transaction
          |
          v
PostgreSQL activity record
          |
          v
Event publisher
          |
          v
WebSocket connection
          |
          v
React activity timeline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The essential principle is that the database remains the source of truth. WebSockets deliver updates quickly, but clients should be able to retrieve historical events from the database if they disconnect.&lt;/p&gt;
&lt;h3 id=&quot;3-designing-the-activity-log-database&quot;&gt;3. Designing the Activity Log Database&lt;/h3&gt;
&lt;p&gt;A reliable activity log starts with a clear database schema. Each record should contain enough information to explain an event without storing unnecessary sensitive data.&lt;/p&gt;
&lt;p&gt;We can create an &lt;code&gt;activity_logs&lt;/code&gt; table with the following fields:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Unique event identifier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;actor_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;User or system responsible for the action&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;action&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Type of activity performed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;entity_type&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Type of affected resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;entity_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Identifier of the affected resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Human-readable event summary&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;metadata&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Additional structured event details&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Timestamp when the event was recorded&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;metadata&lt;/code&gt; field can use PostgreSQL's JSONB type. This makes it possible to store flexible information without creating a new database column for every event type.&lt;/p&gt;
&lt;p&gt;For example, a project update could contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;field&quot;: &quot;deadline&quot;,
  &quot;old_value&quot;: &quot;2026-11-10&quot;,
  &quot;new_value&quot;: &quot;2026-11-15&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avoid storing passwords, authentication tokens, payment card details, or unnecessary personal information in event metadata.&lt;/p&gt;
&lt;h4 id=&quot;creating-the-database-table&quot;&gt;Creating the database table&lt;/h4&gt;
&lt;p&gt;A simple PostgreSQL migration might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE activity_logs (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    actor_id BIGINT NULL,
    action VARCHAR(100) NOT NULL,
    entity_type VARCHAR(100),
    entity_id VARCHAR(100),
    description TEXT NOT NULL,
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_activity_logs_created_at
    ON activity_logs (created_at DESC, id DESC);

CREATE INDEX idx_activity_logs_actor
    ON activity_logs (actor_id, created_at DESC, id DESC);

CREATE INDEX idx_activity_logs_entity
    ON activity_logs (
        entity_type,
        entity_id,
        created_at DESC,
        id DESC
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These indexes support common queries, including retrieving recent events, filtering by actor, and viewing the history of a specific resource.&lt;/p&gt;
&lt;p&gt;In a multi-tenant application, include a &lt;code&gt;tenant_id&lt;/code&gt; or equivalent ownership identifier and design indexes around the actual access patterns.&lt;/p&gt;
&lt;p&gt;The example uses a nullable &lt;code&gt;actor_id&lt;/code&gt; because some events may be generated by scheduled jobs or automated processes. In a production database, define appropriate foreign keys and retention rules according to your user-deletion and audit requirements.&lt;/p&gt;
&lt;h3 id=&quot;4-defining-consistent-activity-events&quot;&gt;4. Defining Consistent Activity Events&lt;/h3&gt;
&lt;p&gt;A major challenge is maintaining consistency across different parts of an application.&lt;/p&gt;
&lt;p&gt;Without a shared event structure, one feature might record &lt;code&gt;&quot;user updated&quot;&lt;/code&gt; while another records &lt;code&gt;&quot;profile_changed&quot;&lt;/code&gt;. The frontend then becomes responsible for understanding many inconsistent formats.&lt;/p&gt;
&lt;p&gt;A better approach is to define a standard event contract.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;evt_1024&quot;,
  &quot;actor&quot;: {
    &quot;id&quot;: 42,
    &quot;name&quot;: &quot;Sarah&quot;
  },
  &quot;action&quot;: &quot;project.updated&quot;,
  &quot;entity_type&quot;: &quot;project&quot;,
  &quot;entity_id&quot;: &quot;project_15&quot;,
  &quot;description&quot;: &quot;Sarah updated the project deadline&quot;,
  &quot;metadata&quot;: {
    &quot;field&quot;: &quot;deadline&quot;
  },
  &quot;created_at&quot;: &quot;2026-10-27T06:30:00Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The action name should describe the event in a predictable way. Names such as &lt;code&gt;project.created&lt;/code&gt;, &lt;code&gt;project.updated&lt;/code&gt;, &lt;code&gt;task.assigned&lt;/code&gt;, and &lt;code&gt;user.permission_changed&lt;/code&gt; are easier to maintain than arbitrary descriptions used as event identifiers.&lt;/p&gt;
&lt;p&gt;Separate the machine-readable action from the human-readable description.&lt;/p&gt;
&lt;p&gt;The action supports filtering, analytics, and integrations. The description is intended for display and can be localized later.&lt;/p&gt;
&lt;p&gt;Also, distinguish between the person who performed an action and the person or object affected by it. For example, an administrator may change another user's permissions.&lt;/p&gt;
&lt;h3 id=&quot;5-building-the-fastapi-activity-service&quot;&gt;5. Building the FastAPI Activity Service&lt;/h3&gt;
&lt;p&gt;The backend should provide reusable functions for creating and retrieving activities. Individual business endpoints should call these functions rather than duplicating database logic.&lt;/p&gt;
&lt;p&gt;The following example illustrates the structure of a service. It assumes that the application has already configured its database session and authentication system.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import datetime, timezone
from typing import Any

from pydantic import BaseModel, Field


class ActivityCreate(BaseModel):
    actor_id: int | None = None
    action: str = Field(min_length=1, max_length=100)
    entity_type: str | None = None
    entity_id: str | None = None
    description: str = Field(min_length=1, max_length=500)
    metadata: dict[str, Any] = Field(default_factory=dict)


class ActivityResponse(BaseModel):
    id: int
    actor_id: int | None
    action: str
    entity_type: str | None
    entity_id: str | None
    description: str
    metadata: dict[str, Any]
    created_at: datetime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pydantic validates the shape of the data before it reaches the service. However, validation alone is not enough. The backend must also determine whether the authenticated actor is allowed to perform the action and whether the supplied resource belongs to the correct tenant.&lt;/p&gt;
&lt;h4 id=&quot;recording-an-activity&quot;&gt;Recording an activity&lt;/h4&gt;
&lt;p&gt;The activity service should insert the event into the database using the same transaction as the business operation whenever practical.&lt;/p&gt;
&lt;p&gt;Conceptually, the service performs the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validate the event data.&lt;/li&gt;
&lt;li&gt;Confirm the actor's identity and permissions.&lt;/li&gt;
&lt;li&gt;Execute the business operation.&lt;/li&gt;
&lt;li&gt;Insert the activity record.&lt;/li&gt;
&lt;li&gt;Commit the transaction.&lt;/li&gt;
&lt;li&gt;Publish the event for real-time delivery.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This ordering matters. Broadcasting an event before the database transaction commits can cause the interface to display a change that is later rolled back.&lt;/p&gt;
&lt;p&gt;For a small application, a database insert followed by publication may be sufficient. For production systems where losing an event is unacceptable, the transactional outbox pattern provides stronger reliability.&lt;/p&gt;
&lt;h3 id=&quot;6-connecting-business-operations-to-the-activity-log&quot;&gt;6. Connecting Business Operations to the Activity Log&lt;/h3&gt;
&lt;p&gt;The most useful logs are generated automatically by meaningful application operations.&lt;/p&gt;
&lt;p&gt;Suppose a user creates a project. The backend should create the project and its activity record together.&lt;/p&gt;
&lt;p&gt;A simplified FastAPI endpoint could follow this pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import APIRouter, Depends, HTTPException

router = APIRouter()


@router.post(&quot;/projects&quot;)
async def create_project(
    payload: dict,
    current_user=Depends(get_current_user),
    db=Depends(get_db),
):
    project = await save_project(db, payload, current_user.id)

    activity = await save_activity(
        db=db,
        actor_id=current_user.id,
        action=&quot;project.created&quot;,
        entity_type=&quot;project&quot;,
        entity_id=str(project.id),
        description=f&quot;{current_user.name} created a project&quot;,
        metadata={&quot;project_name&quot;: project.name},
    )

    await db.commit()

    await publish_activity(activity)

    return project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;get_current_user&lt;/code&gt;, &lt;code&gt;get_db&lt;/code&gt;, &lt;code&gt;save_project&lt;/code&gt;, &lt;code&gt;save_activity&lt;/code&gt;, and &lt;code&gt;publish_activity&lt;/code&gt; represent application-specific functions that must be implemented.&lt;/p&gt;
&lt;p&gt;For true transactional safety, both database writes must use the same transaction, and failures must roll back both operations. The publishing function should also handle delivery failures without incorrectly reporting that a committed business operation failed.&lt;/p&gt;
&lt;p&gt;A stronger design records an outbox message in the same transaction. A separate worker publishes committed messages and retries failed deliveries.&lt;/p&gt;
&lt;h4 id=&quot;which-actions-should-be-recorded-&quot;&gt;Which actions should be recorded?&lt;/h4&gt;
&lt;p&gt;Do not record every technical operation indiscriminately. Focus on events that help users understand changes or help administrators investigate important activity.&lt;/p&gt;
&lt;p&gt;Useful examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating, editing, or deleting a record.&lt;/li&gt;
&lt;li&gt;Assigning a task to a team member.&lt;/li&gt;
&lt;li&gt;Updating an order status.&lt;/li&gt;
&lt;li&gt;Changing account permissions.&lt;/li&gt;
&lt;li&gt;Uploading or removing a document.&lt;/li&gt;
&lt;li&gt;Inviting or removing a user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read-only operations, routine polling requests, and other high-frequency events usually belong in application telemetry rather than the business activity timeline.&lt;/p&gt;
&lt;h3 id=&quot;7-building-the-websocket-endpoint&quot;&gt;7. Building the WebSocket Endpoint&lt;/h3&gt;
&lt;p&gt;REST APIs are useful for retrieving historical activities, but repeatedly polling an endpoint is inefficient when users need immediate updates.&lt;/p&gt;
&lt;p&gt;WebSockets allow the server to push newly published events to connected clients.&lt;/p&gt;
&lt;p&gt;A minimal FastAPI implementation might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import WebSocket, WebSocketDisconnect


class ConnectionManager:
    def __init__(self):
        self.connections: set[WebSocket] = set()

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.connections.add(websocket)

    def disconnect(self, websocket: WebSocket):
        self.connections.discard(websocket)

    async def broadcast(self, event: dict):
        disconnected = []

        for connection in list(self.connections):
            try:
                await connection.send_json(event)
            except Exception:
                disconnected.append(connection)

        for connection in disconnected:
            self.disconnect(connection)


manager = ConnectionManager()


@app.websocket(&quot;/ws/activity&quot;)
async def activity_socket(websocket: WebSocket):
    # Authenticate and authorize the connection before accepting it.
    user = await authenticate_websocket(websocket)

    if user is None:
        await websocket.close(code=1008)
        return

    await manager.connect(websocket)

    try:
        while True:
            # Production implementations should also handle heartbeats,
            # disconnects, and bounded message processing.
            await websocket.receive_text()
    except WebSocketDisconnect:
        manager.disconnect(websocket)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code illustrates the connection lifecycle rather than a complete production implementation. The &lt;code&gt;authenticate_websocket&lt;/code&gt; function must be implemented using your application's authentication strategy.&lt;/p&gt;
&lt;p&gt;The manager broadcasts only to sockets registered with it. In a real multi-user application, a global broadcast to every connection would be unsafe. Store connections by authorized tenant, project, organization, or user, and send each event only to clients entitled to see it.&lt;/p&gt;
&lt;p&gt;Never assume that a user is authorized simply because they successfully established a WebSocket connection.&lt;/p&gt;
&lt;h3 id=&quot;8-implementing-the-react-activity-timeline&quot;&gt;8. Implementing the React Activity Timeline&lt;/h3&gt;
&lt;p&gt;The frontend needs two capabilities: loading existing activities and receiving new ones.&lt;/p&gt;
&lt;p&gt;The historical API might expose an endpoint such as &lt;code&gt;GET /api/activity?limit=20&lt;/code&gt;. The browser can load the most recent records when the page opens, while the WebSocket delivers updates afterward.&lt;/p&gt;
&lt;p&gt;A basic React component could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useEffect, useState } from &quot;react&quot;;

export default function ActivityTimeline() {
  const [activities, setActivities] = useState([]);

  useEffect(() =&amp;gt; {
    let socket;

    async function loadActivities() {
      const response = await fetch(&quot;/api/activity?limit=20&quot;, {
        credentials: &quot;include&quot;,
      });

      if (!response.ok) {
        throw new Error(&quot;Unable to load activity history&quot;);
      }

      const data = await response.json();
      setActivities(data.items);
    }

    loadActivities().catch(console.error);

    socket = new WebSocket(&quot;wss://example.com/ws/activity&quot;);

    socket.onmessage = (message) =&amp;gt; {
      const activity = JSON.parse(message.data);

      setActivities((current) =&amp;gt; {
        if (current.some((item) =&amp;gt; item.id === activity.id)) {
          return current;
        }

        return [activity, ...current].slice(0, 50);
      });
    };

    return () =&amp;gt; {
      socket?.close();
    };
  }, []);

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h2&amp;gt;Recent Activity&amp;lt;/h2&amp;gt;

      &amp;lt;ul&amp;gt;
        {activities.map((activity) =&amp;gt; (
          &amp;lt;li key={activity.id}&amp;gt;
            &amp;lt;strong&amp;gt;{activity.description}&amp;lt;/strong&amp;gt;
            &amp;lt;time dateTime={activity.created_at}&amp;gt;
              {new Date(activity.created_at).toLocaleString()}
            &amp;lt;/time&amp;gt;
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the example domain with your own deployment address. The API response is assumed to contain an &lt;code&gt;items&lt;/code&gt; array, and the WebSocket endpoint must return events matching the activity schema.&lt;/p&gt;
&lt;p&gt;This example intentionally stays small. A production component should handle loading states, API errors, WebSocket reconnection, authentication expiration, and changes to network connectivity.&lt;/p&gt;
&lt;p&gt;For session-cookie authentication, use secure cookie settings and validate the WebSocket handshake's origin. If your system uses bearer tokens, avoid placing long-lived credentials in URLs because URLs can appear in logs and monitoring systems.&lt;/p&gt;
&lt;h4 id=&quot;improving-the-user-experience&quot;&gt;Improving the user experience&lt;/h4&gt;
&lt;p&gt;An activity timeline should be easy to scan. Consider displaying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The actor's name and profile image, where appropriate.&lt;/li&gt;
&lt;li&gt;A short action description.&lt;/li&gt;
&lt;li&gt;A relative timestamp such as &quot;2 minutes ago&quot;.&lt;/li&gt;
&lt;li&gt;A link to the affected resource.&lt;/li&gt;
&lt;li&gt;Filters for event type, user, or date range.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use semantic HTML and accessible labels. Color can communicate event categories, but the interface should not depend on color alone.&lt;/p&gt;
&lt;p&gt;Also, avoid inserting user-generated descriptions as raw HTML. Render them as text or sanitize them appropriately to prevent cross-site scripting.&lt;/p&gt;
&lt;h3 id=&quot;9-retrieving-historical-activities-with-pagination&quot;&gt;9. Retrieving Historical Activities with Pagination&lt;/h3&gt;
&lt;p&gt;An activity table can grow rapidly. Returning every event in one API response will eventually become slow and expensive.&lt;/p&gt;
&lt;p&gt;Use pagination to retrieve only the records the interface needs.&lt;/p&gt;
&lt;p&gt;For smaller datasets, limit-and-offset pagination is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, actor_id, action, entity_type,
       entity_id, description, created_at
FROM activity_logs
WHERE tenant_id = :tenant_id
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query assumes that a &lt;code&gt;tenant_id&lt;/code&gt; column has been added to the table.&lt;/p&gt;
&lt;p&gt;For large datasets, cursor-based pagination is usually preferable. The client supplies the timestamp and ID of the last event it received. The next page uses a condition such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, actor_id, action, entity_type,
       entity_id, description, created_at
FROM activity_logs
WHERE tenant_id = :tenant_id
  AND (created_at, id) &amp;lt; (:cursor_time, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timestamp and ID together provide a stable ordering when multiple activities share the same timestamp.&lt;/p&gt;
&lt;p&gt;Validate pagination parameters on the backend and apply a maximum page size. The database should return only fields the client is allowed to see.&lt;/p&gt;
&lt;h3 id=&quot;10-handling-real-time-events-reliably&quot;&gt;10. Handling Real-Time Events Reliably&lt;/h3&gt;
&lt;p&gt;A WebSocket connection does not guarantee that every event will reach every browser.&lt;/p&gt;
&lt;p&gt;Users may lose their network connection, close a laptop, refresh a page, or move between networks. A server may also restart while clients are connected.&lt;/p&gt;
&lt;p&gt;For that reason, treat real-time delivery as an optimization rather than the permanent storage mechanism.&lt;/p&gt;
&lt;p&gt;A reliable workflow should:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Save the activity record.&lt;/li&gt;
&lt;li&gt;Commit the database transaction.&lt;/li&gt;
&lt;li&gt;Publish an event containing the record identifier and relevant payload.&lt;/li&gt;
&lt;li&gt;Retry publication if delivery fails.&lt;/li&gt;
&lt;li&gt;Allow clients to reconnect.&lt;/li&gt;
&lt;li&gt;Fetch missed activities after reconnection.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For higher reliability, use the &lt;strong&gt;transactional outbox pattern&lt;/strong&gt;. In the same database transaction as the business change, insert an outbox record describing the event. A background worker reads unpublished records, publishes them to a message broker, and marks them as processed.&lt;/p&gt;
&lt;p&gt;If the worker crashes after publishing but before marking a message as processed, it may publish the same event again. Therefore, consumers should be idempotent and use event IDs to avoid duplicate processing.&lt;/p&gt;
&lt;p&gt;Redis Pub/Sub can distribute messages between application instances, but it does not provide durable message storage for disconnected consumers. If events must survive outages, consider a durable queue or streaming system appropriate to your workload.&lt;/p&gt;
&lt;h3 id=&quot;11-security-and-access-control&quot;&gt;11. Security and Access Control&lt;/h3&gt;
&lt;p&gt;Activity logs can contain sensitive information about business operations. Protecting them is as important as protecting the primary application database.&lt;/p&gt;
&lt;h4 id=&quot;authenticate-every-request&quot;&gt;Authenticate every request&lt;/h4&gt;
&lt;p&gt;Require authentication for historical activity endpoints and WebSocket connections. Expired sessions and revoked permissions must be handled correctly.&lt;/p&gt;
&lt;h4 id=&quot;enforce-authorization&quot;&gt;Enforce authorization&lt;/h4&gt;
&lt;p&gt;Filter activity records by tenant, organization, project, or user permissions on the server. Never rely on the React interface to hide records that the user is not allowed to access.&lt;/p&gt;
&lt;h4 id=&quot;prevent-tampering&quot;&gt;Prevent tampering&lt;/h4&gt;
&lt;p&gt;For important audit trails, restrict modification and deletion privileges. Consider append-only database permissions, immutable backups, or external audit storage where appropriate.&lt;/p&gt;
&lt;h4 id=&quot;minimize-sensitive-data&quot;&gt;Minimize sensitive data&lt;/h4&gt;
&lt;p&gt;Do not store passwords, session cookies, API keys, full payment details, or unnecessary personal information in event descriptions or metadata.&lt;/p&gt;
&lt;h4 id=&quot;validate-websocket-origins&quot;&gt;Validate WebSocket origins&lt;/h4&gt;
&lt;p&gt;For cookie-authenticated connections, validate the request origin against an explicit allowlist. Use encrypted connections in production and rate-limit connection attempts.&lt;/p&gt;
&lt;h4 id=&quot;separate-business-events-from-security-logs&quot;&gt;Separate business events from security logs&lt;/h4&gt;
&lt;p&gt;A business activity timeline and a security audit trail may overlap, but they are not identical. Security logs may require stricter access controls, longer retention, reliable ingestion, and records of privileged administrative actions.&lt;/p&gt;
&lt;p&gt;Do not assume that a normal user-facing activity feed is sufficient for regulatory or security auditing.&lt;/p&gt;
&lt;h3 id=&quot;12-performance-optimization-for-large-applications&quot;&gt;12. Performance Optimization for Large Applications&lt;/h3&gt;
&lt;p&gt;As the number of users grows, the volume of activity events can increase significantly. A design that works for a small prototype may struggle under a large workload.&lt;/p&gt;
&lt;h4 id=&quot;use-appropriate-database-indexes&quot;&gt;Use appropriate database indexes&lt;/h4&gt;
&lt;p&gt;Index fields used for filtering and ordering. Avoid adding indexes without a clear query pattern because every additional index increases storage requirements and write costs.&lt;/p&gt;
&lt;h4 id=&quot;limit-websocket-payloads&quot;&gt;Limit WebSocket payloads&lt;/h4&gt;
&lt;p&gt;Send only the fields the recipient needs. Large metadata objects increase network traffic and browser processing time.&lt;/p&gt;
&lt;h4 id=&quot;avoid-blocking-database-operations&quot;&gt;Avoid blocking database operations&lt;/h4&gt;
&lt;p&gt;Use asynchronous database access where appropriate, connection pooling, and carefully managed transaction boundaries.&lt;/p&gt;
&lt;h4 id=&quot;batch-or-queue-publication&quot;&gt;Batch or queue publication&lt;/h4&gt;
&lt;p&gt;If many events are generated in a short period, a queue can absorb bursts and allow workers to publish at a controlled rate.&lt;/p&gt;
&lt;h4 id=&quot;apply-retention-policies&quot;&gt;Apply retention policies&lt;/h4&gt;
&lt;p&gt;Not every event needs to remain in the primary database forever. Depending on business requirements, older records may be archived, partitioned, or deleted after an approved retention period.&lt;/p&gt;
&lt;p&gt;Never introduce a retention policy that conflicts with legal, contractual, or audit obligations.&lt;/p&gt;
&lt;h4 id=&quot;monitor-important-metrics&quot;&gt;Monitor important metrics&lt;/h4&gt;
&lt;p&gt;Track event creation latency, publication failures, queue depth, WebSocket connection counts, query duration, and database storage growth.&lt;/p&gt;
&lt;p&gt;These metrics help identify bottlenecks before users experience slow timelines or missing updates.&lt;/p&gt;
&lt;h3 id=&quot;13-testing-the-activity-log&quot;&gt;13. Testing the Activity Log&lt;/h3&gt;
&lt;p&gt;Testing should cover both the stored history and real-time behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unit tests:&lt;/strong&gt; Verify event validation, action naming, metadata handling, and formatting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integration tests:&lt;/strong&gt; Confirm that a successful business operation creates the expected activity record and that a failed transaction does not leave a misleading record.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebSocket tests:&lt;/strong&gt; Check that authorized clients receive relevant events and unauthorized clients cannot subscribe to restricted activity streams.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pagination tests:&lt;/strong&gt; Verify stable ordering when timestamps match and ensure that cursor-based requests do not unexpectedly repeat records.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reconnect tests:&lt;/strong&gt; Simulate a dropped connection, reconnect, and fetch missed events.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security tests:&lt;/strong&gt; Check tenant isolation, authorization changes, origin validation, and attempts to inject unsafe content.&lt;/p&gt;
&lt;p&gt;One especially important test is whether a client can request another tenant's activity by changing a URL parameter or subscribing to a different WebSocket channel. The server must reject unauthorized access regardless of what the frontend displays.&lt;/p&gt;
&lt;h3 id=&quot;14-common-mistakes-to-avoid&quot;&gt;14. Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Several implementation mistakes can make an activity log unreliable or unsafe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publishing before committing:&lt;/strong&gt; The browser may display an event for a transaction that later fails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broadcasting to every connected user:&lt;/strong&gt; Private activities may be disclosed to unauthorized clients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using WebSockets as the only data store:&lt;/strong&gt; Disconnected clients can miss events permanently unless they can retrieve history.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Logging every request:&lt;/strong&gt; Excessive noise makes meaningful activity difficult to find and increases storage costs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storing entire database records:&lt;/strong&gt; This can expose sensitive fields and produce large event payloads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring duplicate delivery:&lt;/strong&gt; Retries can cause the same activity to appear multiple times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using unstable pagination:&lt;/strong&gt; Ordering only by timestamp can produce inconsistent pages when several events share the same timestamp.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skipping authorization checks:&lt;/strong&gt; A valid login does not automatically grant access to every project or organization.&lt;/p&gt;
&lt;p&gt;Avoiding these mistakes early makes the system easier to maintain as the application grows.&lt;/p&gt;
&lt;h3 id=&quot;15-deployment-considerations&quot;&gt;15. Deployment Considerations&lt;/h3&gt;
&lt;p&gt;Before deploying the activity service, configure the infrastructure for production workloads.&lt;/p&gt;
&lt;p&gt;Use environment variables or a managed secrets system for database credentials. Enable TLS for HTTPS and WSS traffic. Configure connection limits, request timeouts, database pooling, and reverse-proxy WebSocket support.&lt;/p&gt;
&lt;p&gt;If multiple FastAPI instances are running, remember that an in-memory connection manager only knows about connections attached to its own process. Use shared messaging infrastructure so events can reach clients connected to different instances.&lt;/p&gt;
&lt;p&gt;Configure health checks for the API and background workers. Monitor failed publications and make sure the outbox or queue can be recovered after an outage.&lt;/p&gt;
&lt;p&gt;Finally, test deployment behavior under realistic conditions. Restart application instances while users are connected, simulate slow clients, and confirm that historical events remain available after reconnecting.&lt;/p&gt;
&lt;h3 id=&quot;16-future-enhancements&quot;&gt;16. Future Enhancements&lt;/h3&gt;
&lt;p&gt;Once the basic activity log is working, several improvements can make it more useful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced filtering:&lt;/strong&gt; Allow users to filter by actor, event category, project, or date range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search:&lt;/strong&gt; Add full-text search for descriptions and selected metadata fields.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Notifications:&lt;/strong&gt; Turn important activity events into targeted notifications or email alerts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Export functionality:&lt;/strong&gt; Allow authorized users to export filtered activity records for reporting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Analytics:&lt;/strong&gt; Aggregate event data to understand product usage and team collaboration patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit history:&lt;/strong&gt; Provide detailed before-and-after values for selected changes, with appropriate redaction and access controls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live indicators:&lt;/strong&gt; Show a small notification badge when new events arrive and let users decide when to load them into the current timeline.&lt;/p&gt;
&lt;p&gt;Each feature should be built around a clear requirement. Avoid adding complexity that does not improve the experience or meet an operational need.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a real-time activity log is more than connecting a WebSocket to a React component. A dependable implementation combines a well-designed database schema, consistent event definitions, secure authorization, reliable persistence, and a frontend that can handle new events and network interruptions.&lt;/p&gt;
&lt;p&gt;React provides the interactive timeline, FastAPI manages application operations and real-time connections, and PostgreSQL stores the permanent activity history. WebSockets deliver immediate updates, while pagination, database indexes, and background workers help the architecture scale.&lt;/p&gt;
&lt;p&gt;For a small application, begin with a structured activity table, a reusable event service, a historical REST endpoint, and a secured WebSocket connection. As requirements grow, introduce cursor-based pagination, a transactional outbox, shared messaging, retention policies, and stronger audit controls.&lt;/p&gt;
&lt;p&gt;The ultimate goal is to make application activity transparent, useful, and trustworthy. When implemented correctly, a real-time activity log helps users understand changes, gives administrators better visibility, and makes complex web applications easier to maintain.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32629/how-to-build-a-real-time-activity-log-for-your-web-app</guid>
<pubDate>Tue, 27 Oct 2026 06:15:00 +0000</pubDate>
</item>
<item>
<title>I Made AURA: A Windows 10 L2TP/IPsec VPN Client Written From Scratch in Python</title>
<link>https://coderlegion.com/32556/i-made-aura-a-windows-10-l2tp-ipsec-vpn-client-written-from-scratch-in-python</link>
<description>&lt;p&gt;For work I needed to connect to a private network over L2TP/IPsec. The VPN client built into Windows 10 refused to connect at all, and it gave me nothing to debug with: no log of the key exchange, no hint about which step failed. I went looking for another program that could do the job and found nothing suitable&lt;/p&gt;
&lt;p&gt;This became the reason for creating AURA — a VPN client that speaks the whole protocol itself, so when something goes wrong you can see exactly where&lt;/p&gt;
&lt;h3 id=&quot;-what-is-it-&quot;&gt;🔐 What is it?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AURA&lt;/strong&gt; is a Windows 10 &lt;strong&gt;L2TP/IPsec VPN client&lt;/strong&gt; with its own IKEv1, ESP, L2TP and PPP stack, written in pure Python with PyQt5. The traffic goes through a &lt;strong&gt;Wintun&lt;/strong&gt; virtual adapter, the driver from the WireGuard project. Nothing of Windows' built-in VPN client is used: no RAS connection, no &lt;code&gt;rasdial&lt;/code&gt;, no Windows VPN service. The program needs administrator rights at launch, because the adapter, routes and DNS are system changes, and asks for them once through UAC&lt;/p&gt;
&lt;h3 id=&quot;-screenshots&quot;&gt;🖥️ Screenshots&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16364643041434206570&quot; alt=&quot;Pic. 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6780537254809234169&quot; alt=&quot;Pic. 2&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;-how-does-it-work-&quot;&gt;⚙️ How does it work?&lt;/h3&gt;
&lt;p&gt;You enter a server, a username, a password and, when the server wants one, a pre-shared key, then press CONNECT. AURA offers the server 18 IKE proposals (AES/3DES, SHA-256/SHA-1, DH 14/5/2) and uses whichever it picks, opens one L2TP tunnel and session, authenticates with MS-CHAPv2, CHAP or PAP, whatever the server asks for, and gets its address and DNS over IPCP. The status bar and the log panel walk through every stage&lt;/p&gt;
&lt;p&gt;The most interesting part was a bug I could not see. After a successful key exchange the server's replies were reaching the network card, but my socket received nothing. Windows hands ESP packets that arrive on UDP port 4500 to its own IPsec driver, and a user-mode socket bound to that port sees only the IKE messages. The fix was to send from an ephemeral local port: the server answers whatever port it observes&lt;/p&gt;
&lt;p&gt;Because the program changes routes and DNS, every change is recorded before it is made, and the next launch removes whatever a crash left behind. Your credentials stay on your machine: a password and a pre-shared key are written to disk only when you tick Remember, as a DPAPI blob that opens for your Windows account alone, and they never reach the log&lt;/p&gt;
&lt;h3 id=&quot;-key-features-&quot;&gt;✨ Key Features:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🔐 Own IKEv1/IPsec Stack&lt;/strong&gt;&lt;br&gt;
Main and quick mode with a pre-shared key, NAT traversal and dead peer detection. Offers 18 proposals (AES/3DES, SHA-256/SHA-1, DH 14/5/2) and uses whichever the server picks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🧵 L2TP + PPP&lt;/strong&gt;&lt;br&gt;
One tunnel and session with keepalive, then MS-CHAPv2 / CHAP / PAP (whatever the server asks for), IPCP for the address and DNS, and MPPE where a server demands it&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🛡️ Wintun Virtual Adapter&lt;/strong&gt;&lt;br&gt;
Traffic goes through a real Wintun adapter named AURA. Route all sends everything through the tunnel, or list your own networks in &lt;code&gt;extraRoutes&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🗂️ Connection Profiles&lt;/strong&gt;&lt;br&gt;
Several saved servers in a combo box. Password and pre-shared key are kept only on request, protected with DPAPI&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🧹 Crash Recovery Sweep&lt;/strong&gt;&lt;br&gt;
Every route, DNS entry and the adapter are recorded before they change, and the next launch removes whatever a crash left behind&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-roadmap-&quot;&gt;🗺️ Roadmap:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In-place IPsec Rekey&lt;/strong&gt; — Renew the security association before it expires. Today the session ends when the server drops it&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IKEv2&lt;/strong&gt; — A second protocol next to IKEv1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Certificates&lt;/strong&gt; — Authentication beyond the pre-shared key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IPv6&lt;/strong&gt; — Addresses and routes inside the tunnel&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tray Icon&lt;/strong&gt; — Live in the notification area&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic Reconnect&lt;/strong&gt; — Come back after a lost link&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kill Switch&lt;/strong&gt; — No traffic outside the tunnel while it is meant to be up&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;L2TP Tunnel Secret&lt;/strong&gt; — Servers that authenticate the tunnel itself with a shared secret are refused today&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-repository-link&quot;&gt;🔗 Repository Link&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/m-o-z-z-i-x/AURA&quot;&gt;https://github.com/m-o-z-z-i-x/AURA&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32556/i-made-aura-a-windows-10-l2tp-ipsec-vpn-client-written-from-scratch-in-python</guid>
<pubDate>Mon, 26 Oct 2026 13:22:30 +0000</pubDate>
</item>
<item>
<title>Validate AI Video Reference Images with Python Before Generation</title>
<link>https://coderlegion.com/32496/validate-ai-video-reference-images-with-python-before-generation</link>
<description>&lt;p&gt;A reference image can look perfectly fine in a browser and still cause problems in an AI video workflow.&lt;/p&gt;
&lt;p&gt;Perhaps a character portrait was accidentally exported at 320 × 320 pixels. Two reference files might contain exactly the same image under different filenames. Or a damaged image might still exist in the project folder even though it can no longer be opened properly.&lt;/p&gt;
&lt;p&gt;These are problems we can catch before sending anything to a video generation tool.&lt;/p&gt;
&lt;p&gt;In this tutorial, we'll build a small Python validator that checks image formats, dimensions, aspect ratios, file integrity, and duplicate content.&lt;/p&gt;
&lt;p&gt;We'll also generate test images to verify that the script actually works.&lt;/p&gt;
&lt;p&gt;The solution uses Pillow and Python's standard library. No API key or AI video service is required.&lt;/p&gt;
&lt;h3 id=&quot;1-why-validate-reference-images-&quot;&gt;1. Why Validate Reference Images?&lt;/h3&gt;
&lt;p&gt;Reference-guided video generation often involves several assets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Character portraits&lt;/li&gt;
&lt;li&gt;Product photographs&lt;/li&gt;
&lt;li&gt;Background images&lt;/li&gt;
&lt;li&gt;Visual style references&lt;/li&gt;
&lt;li&gt;Storyboard frames&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As a project grows, these files may be renamed, resized, replaced, or duplicated.&lt;/p&gt;
&lt;p&gt;A basic validation step can catch some of these mistakes before generation begins.&lt;/p&gt;
&lt;p&gt;For this example, our validator will check five things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Whether the image uses a supported format.&lt;/li&gt;
&lt;li&gt;Whether it can be opened and decoded.&lt;/li&gt;
&lt;li&gt;Whether its dimensions meet a configurable minimum.&lt;/li&gt;
&lt;li&gt;Whether its aspect ratio matches an optional requirement.&lt;/li&gt;
&lt;li&gt;Whether another file has identical contents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These checks do not guarantee better AI video output. They simply help ensure the input files meet a known set of requirements.&lt;/p&gt;
&lt;h3 id=&quot;2-set-up-the-project&quot;&gt;2. Set Up the Project&lt;/h3&gt;
&lt;p&gt;Install Pillow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install Pillow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the following project structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;video-project/
├── references/
├── check_references.py
└── make_test_images.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We'll first create the validator, then generate several images to test it.&lt;/p&gt;
&lt;h3 id=&quot;3-build-the-image-validator&quot;&gt;3. Build the Image Validator&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;check_references.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import argparse
import hashlib
import json
import warnings
from pathlib import Path

from PIL import Image, UnidentifiedImageError

ALLOWED = {
    &quot;.png&quot;: &quot;PNG&quot;,
    &quot;.jpg&quot;: &quot;JPEG&quot;,
    &quot;.jpeg&quot;: &quot;JPEG&quot;,
    &quot;.webp&quot;: &quot;WEBP&quot;,
}


def fingerprint(path):
    h = hashlib.sha256()

    with path.open(&quot;rb&quot;) as fh:
        for chunk in iter(
            lambda: fh.read(1024 * 1024), b&quot;&quot;
        ):
            h.update(chunk)

    return h.hexdigest()


def inspect(path, minimum, ratio):
    errors = []

    try:
        expected = ALLOWED.get(
            path.suffix.lower()
        )

        if expected is None:
            return {
                &quot;file&quot;: path.name,
                &quot;errors&quot;: [&quot;Unsupported extension&quot;],
            }

        # Check file structure and decode image data.
        with warnings.catch_warnings():
            warnings.simplefilter(
                &quot;error&quot;,
                Image.DecompressionBombWarning
            )

            with Image.open(path) as image:
                actual = image.format
                width, height = image.size
                image.verify()

            # Reopen after verify() for decoding.
            with Image.open(path) as image:
                image.load()

        if actual != expected:
            errors.append(
                f&quot;Extension/format mismatch: {actual}&quot;
            )

        if min(width, height) &amp;lt; minimum:
            errors.append(
                f&quot;Too small: {width}x{height}&quot;
            )

        if ratio is not None:
            actual_ratio = width / height

            if abs(actual_ratio - ratio) &amp;gt; 0.03:
                errors.append(
                    f&quot;Unexpected ratio: {actual_ratio:.3f}&quot;
                )

        return {
            &quot;file&quot;: path.name,
            &quot;size&quot;: [width, height],
            &quot;sha256&quot;: fingerprint(path),
            &quot;errors&quot;: errors,
        }

    except (
        OSError,
        ValueError,
        UnidentifiedImageError,
        Image.DecompressionBombError,
        Image.DecompressionBombWarning,
    ) as exc:
        return {
            &quot;file&quot;: path.name,
            &quot;errors&quot;: [f&quot;Unreadable image: {exc}&quot;],
        }


def main():
    parser = argparse.ArgumentParser()

    parser.add_argument(
        &quot;directory&quot;,
        type=Path
    )

    parser.add_argument(
        &quot;--min-side&quot;,
        type=int,
        default=512
    )

    parser.add_argument(
        &quot;--ratio&quot;,
        type=float
    )

    args = parser.parse_args()

    if (
        args.min_side &amp;lt; 1
        or (
            args.ratio is not None
            and args.ratio &amp;lt;= 0
        )
    ):
        parser.error(&quot;Invalid size or ratio&quot;)

    if not args.directory.is_dir():
        parser.error(&quot;Directory not found&quot;)

    paths = sorted(
        path
        for path in args.directory.iterdir()
        if path.is_file()
    )

    if not paths:
        parser.error(&quot;Directory is empty&quot;)

    seen = {}
    results = []

    for path in paths:
        item = inspect(
            path,
            args.min_side,
            args.ratio
        )

        digest = item.get(&quot;sha256&quot;)

        if digest:
            if digest in seen:
                item[&quot;errors&quot;].append(
                    f&quot;Byte-identical to {seen[digest]}&quot;
                )
            else:
                seen[digest] = path.name

        results.append(item)

    print(json.dumps(results, indent=2))

    return (
        1 if any(item[&quot;errors&quot;] for item in results)
        else 0
    )


if __name__ == &quot;__main__&quot;:
    raise SystemExit(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script processes each file in the selected directory and generates a JSON report.&lt;/p&gt;
&lt;p&gt;It returns exit code &lt;code&gt;0&lt;/code&gt; when every file passes, or &lt;code&gt;1&lt;/code&gt; when one or more files fail.&lt;/p&gt;
&lt;p&gt;That makes it usable in local scripts and automated pipelines.&lt;/p&gt;
&lt;h3 id=&quot;4-generate-test-images&quot;&gt;4. Generate Test Images&lt;/h3&gt;
&lt;p&gt;We should not assume the validator works just because the code runs.&lt;/p&gt;
&lt;p&gt;Let's create files that exercise its main checks.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;make_test_images.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pathlib import Path
from shutil import copyfile

from PIL import Image

folder = Path(&quot;references&quot;)
folder.mkdir(exist_ok=True)

# A valid image.
Image.new(
    &quot;RGB&quot;,
    (1024, 1024),
    &quot;white&quot;
).save(folder / &quot;valid.png&quot;)

# An undersized image.
Image.new(
    &quot;RGB&quot;,
    (320, 320),
    &quot;red&quot;
).save(folder / &quot;small.png&quot;)

# A byte-identical duplicate.
copyfile(
    folder / &quot;valid.png&quot;,
    folder / &quot;valid_copy.png&quot;
)

# A deliberately invalid image file.
(folder / &quot;broken.png&quot;).write_bytes(
    b&quot;not a real image&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python make_test_images.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your &lt;code&gt;references&lt;/code&gt; directory should now contain four files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;references/
├── valid.png
├── valid_copy.png
├── small.png
└── broken.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These test assets allow us to check several error conditions without downloading external images.&lt;/p&gt;
&lt;h3 id=&quot;5-run-the-validator&quot;&gt;5. Run the Validator&lt;/h3&gt;
&lt;p&gt;Execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python check_references.py references
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The validator should identify:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;File &lt;/th&gt;&lt;th&gt;Expected result&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;valid.png&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Valid size and format&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;valid_copy.png&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Exact duplicate detected&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;small.png&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Too small: 320x320&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;broken.png&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Unreadable image&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The duplicate may be reported against either filename, depending on the order in which the files are processed.&lt;/p&gt;
&lt;h4 id=&quot;what-happens-when-everything-is-valid-&quot;&gt;What happens when everything is valid?&lt;/h4&gt;
&lt;p&gt;Remove the broken, small, and duplicate files, leaving only &lt;code&gt;valid.png&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Run the validator again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python check_references.py references
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result should contain an empty &lt;code&gt;errors&lt;/code&gt; list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;file&quot;: &quot;valid.png&quot;,
  &quot;size&quot;: [1024, 1024],
  &quot;sha256&quot;: &quot;...&quot;,
  &quot;errors&quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SHA-256 value is abbreviated here for readability. The actual script prints the complete hash.&lt;/p&gt;
&lt;p&gt;The process should also exit successfully.&lt;/p&gt;
&lt;h4 id=&quot;test-a-custom-minimum-size&quot;&gt;Test a custom minimum size&lt;/h4&gt;
&lt;p&gt;You can change the minimum dimensions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python check_references.py references --min-side 768
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires both dimensions to be at least 768 pixels.&lt;/p&gt;
&lt;p&gt;It is a project-level rule, not a universal requirement imposed by AI video models.&lt;/p&gt;
&lt;p&gt;A character portrait and a wide landscape background may need different dimension policies.&lt;/p&gt;
&lt;h4 id=&quot;test-an-aspect-ratio&quot;&gt;Test an aspect ratio&lt;/h4&gt;
&lt;p&gt;For a project expecting roughly 16:9 images:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python check_references.py references --ratio 1.777778
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script allows a difference of 0.03 between the actual and requested width-to-height ratios.&lt;/p&gt;
&lt;p&gt;This is useful for a consistent background-image set, but shouldn't automatically be applied to every reference image.&lt;/p&gt;
&lt;p&gt;Portraits, objects, and backgrounds often need different framing.&lt;/p&gt;
&lt;h3 id=&quot;6-why-use-sha-256-for-duplicate-detection-&quot;&gt;6. Why Use SHA-256 for Duplicate Detection?&lt;/h3&gt;
&lt;p&gt;The validator calculates a SHA-256 fingerprint for every successfully inspected image.&lt;/p&gt;
&lt;p&gt;If two files have identical bytes, their hashes match.&lt;/p&gt;
&lt;p&gt;This catches a common asset-management mistake: copying an image under another filename and accidentally including both files in a reference set.&lt;/p&gt;
&lt;p&gt;However, SHA-256 is not a visual similarity detector.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Two identical files with different names will be detected.&lt;/li&gt;
&lt;li&gt;A JPEG recompressed with different settings will usually produce a different hash.&lt;/li&gt;
&lt;li&gt;An image with modified metadata may have a different hash.&lt;/li&gt;
&lt;li&gt;Two images showing the same subject at different resolutions will generally not match.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For visual similarity detection, teams would need a separate method such as perceptual hashing.&lt;/p&gt;
&lt;p&gt;The choice depends on the problem being solved.&lt;/p&gt;
&lt;p&gt;If the goal is to find exact duplicate files, SHA-256 is simple and reliable.&lt;/p&gt;
&lt;h3 id=&quot;7-use-validation-before-ai-video-generation&quot;&gt;7. Use Validation Before AI Video Generation&lt;/h3&gt;
&lt;p&gt;Reference-heavy video projects often use separate images to guide characters, environments, objects, and camera composition.&lt;/p&gt;
&lt;p&gt;Platforms such as &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.kling3ai.net/kling/kling-4&quot;&gt;Kling 4.0&lt;/a&gt; describe reference-guided generation and multi-keyframe controls, making reference preparation a relevant part of the workflow.&lt;/p&gt;
&lt;p&gt;The validator operates entirely on local files. It does not connect to Kling 4.0 or require a provider-specific API.&lt;/p&gt;
&lt;p&gt;It can detect invalid or duplicate files before upload, while actual reference limits, accepted formats, and model requirements must be confirmed through the selected service.&lt;/p&gt;
&lt;p&gt;A practical workflow might be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Collect reference images in a project directory.&lt;/li&gt;
&lt;li&gt;Validate file formats and dimensions.&lt;/li&gt;
&lt;li&gt;Identify duplicates and corrupted files.&lt;/li&gt;
&lt;li&gt;Review asset permissions.&lt;/li&gt;
&lt;li&gt;Upload only the approved references.&lt;/li&gt;
&lt;li&gt;Review the generated video for visual accuracy.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Local validation reduces avoidable input problems, but it does not guarantee that a generated character or object will remain consistent.&lt;/p&gt;
&lt;h3 id=&quot;8-production-considerations&quot;&gt;8. Production Considerations&lt;/h3&gt;
&lt;p&gt;This validator is deliberately small.&lt;/p&gt;
&lt;p&gt;For a production asset pipeline, several additions may be useful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EXIF orientation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some photographs include orientation metadata that changes how they are displayed. The script checks stored image dimensions rather than applying EXIF-based rotation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File-size and pixel limits&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Large or malicious image files can consume significant memory during decoding. Production systems handling untrusted files should enforce resource limits and use isolated processing environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Different reference roles&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A character portrait, product image, and background scene should not necessarily share the same minimum dimensions or aspect-ratio rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Asset permissions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A technically valid reference is not automatically authorized for upload.&lt;/p&gt;
&lt;p&gt;Teams should confirm that they have permission to use customer photographs, internal screenshots, copyrighted artwork, and images of recognizable people.&lt;/p&gt;
&lt;p&gt;Removing private information does not necessarily make confidential business assets eligible for external processing.&lt;/p&gt;
&lt;p&gt;These issues matter just as much as file dimensions when a tool becomes part of a real production workflow.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;AI video generation involves creative uncertainty, but basic reference-file validation is predictable.&lt;/p&gt;
&lt;p&gt;Before using generation credits, a team can check whether its images are readable, large enough for the intended purpose, correctly formatted, and free of exact duplicates.&lt;/p&gt;
&lt;p&gt;The process does not guarantee better motion, perfect character continuity, or accurate generated details.&lt;/p&gt;
&lt;p&gt;It simply removes a few preventable mistakes before the creative work begins.&lt;/p&gt;
&lt;p&gt;For teams handling many reference files, that's a useful place to introduce a small amount of automation.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32496/validate-ai-video-reference-images-with-python-before-generation</guid>
<pubDate>Mon, 26 Oct 2026 10:47:37 +0000</pubDate>
</item>
<item>
<title>Prepare a driving clip for Wan Animate 2 with ffprobe and ffmpeg: trim, crop, re-check</title>
<link>https://coderlegion.com/32466/prepare-a-driving-clip-for-wan-animate-2-with-ffprobe-and-ffmpeg-trim-crop-re-check</link>
<description>&lt;p&gt;Character animation from a still image has a simple contract: one image says what the character looks like, one video says how it moves. Most failed attempts are not about the model at all. The driving clip is too long, the aspect ratio does not match the character, or the file breaks a size rule the upload form will reject anyway.&lt;/p&gt;
&lt;p&gt;This post walks through a small Python script that checks both inputs against the published rules and prints the exact ffmpeg commands that fix what it finds. Everything below was run locally on a synthetic character image and an ffmpeg test pattern. No generation job was submitted.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7887685804517602179&quot; alt=&quot;Table of input rules for the character image and the driving video&quot;&gt;&lt;br&gt;
&lt;em&gt;Diagram made for this article from the rules on the page.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-rules-worth-encoding&quot;&gt;The rules worth encoding&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://wan30.io/wan-animate-2?utm_source=coderlegion&amp;amp;utm_medium=backlink&amp;amp;utm_campaign=wan30&quot;&gt;Wan Animate 2&lt;/a&gt; page lists the input requirements in its upload panel and step-by-step section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Character image:&lt;/strong&gt; one JPG, JPEG or PNG, up to 10 MB, both sides greater than 340 px, aspect ratio between 2:5 and 5:2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Driving video:&lt;/strong&gt; one MP4 or MOV, up to 100 MB, both sides greater than 340 px, same aspect range.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duration depends on orientation:&lt;/strong&gt; following the video's orientation accepts 3–30 second clips; following the image's orientation accepts 3–10 seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Other controls:&lt;/strong&gt; background from the image or the video, 720p or 1080p output, and an optional prompt of up to 2,500 characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The orientation rule is the one people miss. It changes the allowed clip length, so the checker takes it as an argument instead of assuming one value.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14369558526386659171&quot; alt=&quot;Two boxes comparing follow video and follow image&quot;&gt;&lt;br&gt;
&lt;em&gt;Diagram made for this article.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-script&quot;&gt;The script&lt;/h3&gt;
&lt;p&gt;It needs Pillow and &lt;code&gt;ffprobe&lt;/code&gt;/&lt;code&gt;ffmpeg&lt;/code&gt; on your PATH. Save it as &lt;code&gt;animate_prep.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;animate_prep.py - check a character image + driving video against the
input rules published on the Wan Animate 2 page, and print ffmpeg fixes.

Rules (edit when the page changes):
  image: JPG/JPEG/PNG, &amp;lt;= 10 MB, both sides &amp;gt; 340 px, aspect 2:5..5:2
  video: MP4/MOV, &amp;lt;= 100 MB, both sides &amp;gt; 340 px, aspect 2:5..5:2
         3-30 s when orientation follows the video, 3-10 s when it follows the image
Needs Pillow and ffprobe/ffmpeg on PATH. Nothing is uploaded.
&quot;&quot;&quot;
import argparse, json, os, subprocess, sys
from PIL import Image

MB = 1024 * 1024
IMG_EXT, VID_EXT = {&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.png&quot;}, {&quot;.mp4&quot;, &quot;.mov&quot;}
MIN_SIDE, AR_MIN, AR_MAX = 340, 2 / 5, 5 / 2
DUR = {&quot;video&quot;: (3.0, 30.0), &quot;image&quot;: (3.0, 10.0)}

def probe(path):
    out = subprocess.run([&quot;ffprobe&quot;, &quot;-v&quot;, &quot;error&quot;, &quot;-select_streams&quot;, &quot;v:0&quot;,
                          &quot;-show_entries&quot;, &quot;stream=width,height:format=duration&quot;,
                          &quot;-of&quot;, &quot;json&quot;, path], capture_output=True, text=True, check=True).stdout
    j = json.loads(out)
    s = j[&quot;streams&quot;][0]
    return s[&quot;width&quot;], s[&quot;height&quot;], float(j[&quot;format&quot;][&quot;duration&quot;])

def check_dims(tag, w, h, errs):
    if min(w, h) &amp;lt;= MIN_SIDE:
        errs.append(f&quot;{tag}: {w}x{h} - both sides must be &amp;gt; {MIN_SIDE} px&quot;)
    if not AR_MIN &amp;lt;= w / h &amp;lt;= AR_MAX:
        errs.append(f&quot;{tag}: aspect {w / h:.2f} outside 2:5..5:2&quot;)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument(&quot;image&quot;); ap.add_argument(&quot;video&quot;)
    ap.add_argument(&quot;--orientation&quot;, choices=[&quot;video&quot;, &quot;image&quot;], default=&quot;image&quot;)
    ap.add_argument(&quot;--start&quot;, type=float, default=0.0, help=&quot;where the useful motion begins (s)&quot;)
    a = ap.parse_args()
    errs, fixes = [], []

    ext = os.path.splitext(a.image)[1].lower()
    if ext not in IMG_EXT: errs.append(f&quot;image: {ext} not in {sorted(IMG_EXT)}&quot;)
    if os.path.getsize(a.image) &amp;gt; 10 * MB: errs.append(&quot;image: larger than 10 MB&quot;)
    with Image.open(a.image) as im: iw, ih = im.size
    check_dims(&quot;image&quot;, iw, ih, errs)

    ext = os.path.splitext(a.video)[1].lower()
    if ext not in VID_EXT: errs.append(f&quot;video: {ext} not in {sorted(VID_EXT)}&quot;)
    if os.path.getsize(a.video) &amp;gt; 100 * MB: errs.append(&quot;video: larger than 100 MB&quot;)
    vw, vh, dur = probe(a.video)
    check_dims(&quot;video&quot;, vw, vh, errs)
    lo, hi = DUR[a.orientation]
    if not lo &amp;lt;= dur &amp;lt;= hi:
        errs.append(f&quot;video: {dur:.1f} s outside {lo:.0f}-{hi:.0f} s for orientation={a.orientation}&quot;)
        keep = min(hi, max(lo, dur - a.start))
        fixes.append(f&quot;ffmpeg -y -ss {a.start:g} -i {a.video} -t {keep:g} -c:v libx264 -an trimmed.mp4&quot;)

    # match the driving clip's framing to the character image (centre crop)
    ia, va = iw / ih, vw / vh
    if abs(ia - va) &amp;gt; 0.05:
        if va &amp;gt; ia:  # video too wide -&amp;gt; crop width
            cw, ch = int(vh * ia) // 2 * 2, vh
        else:        # video too tall -&amp;gt; crop height
            cw, ch = vw, int(vw / ia) // 2 * 2
        src = &quot;trimmed.mp4&quot; if fixes else a.video
        fixes.append(f&quot;ffmpeg -y -i {src} -vf crop={cw}:{ch} -c:v libx264 -an framed.mp4&quot;
                     f&quot;   # {vw}x{vh} -&amp;gt; {cw}x{ch}, aspect {ia:.2f} like the image&quot;)

    print(f&quot;image {iw}x{ih} aspect {ia:.2f} | video {vw}x{vh} aspect {va:.2f} {dur:.1f} s | orientation={a.orientation}&quot;)
    for e in errs: print(&quot;FAIL&quot;, e)
    for f in fixes: print(&quot;FIX &quot;, f)
    if not errs and not fixes: print(&quot;PASS ready for upload&quot;)
    sys.exit(1 if errs else 0)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three things it does that a plain size check does not:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reads real dimensions and duration with ffprobe.&lt;/strong&gt; File names and container metadata lie; the stream does not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prints fixes instead of applying them.&lt;/strong&gt; You decide where the useful motion starts (&lt;code&gt;--start&lt;/code&gt;), and you can paste the command or tweak it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matches the clip to the character, not just to the rules.&lt;/strong&gt; A 16:9 clip technically passes the 2:5–5:2 range, but if your character image is a 2:3 portrait, the page itself advises matching the framing and body position of the image and the motion reference. The script suggests a centre crop to the image's aspect ratio when the two differ by more than 0.05.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;run-1-a-14-second-landscape-clip&quot;&gt;Run 1: a 14-second landscape clip&lt;/h3&gt;
&lt;p&gt;The test inputs are a flat 768×1152 character drawing and a 14-second 1920×1080 &lt;code&gt;testsrc2&lt;/code&gt; clip, with the useful motion assumed to start at 2 seconds. With orientation following the image, the clip is too long, and its aspect ratio is far from the portrait character.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12646529945775405748&quot; alt=&quot;Terminal output of the first run with one failure and two fixes&quot;&gt;&lt;br&gt;
&lt;em&gt;Real output from a run on 2026-10-26.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The exit code is 1, so this can gate a script or a Makefile target.&lt;/p&gt;
&lt;h3 id=&quot;apply-the-fixes&quot;&gt;Apply the fixes&lt;/h3&gt;
&lt;p&gt;The two printed commands trim to 10 seconds from the 2-second mark, then crop the centre 720×1080 region so the clip has the same 0.67 aspect ratio as the character image.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1829993883448008614&quot; alt=&quot;Frames before and after the crop next to the character image&quot;&gt;&lt;br&gt;
&lt;em&gt;Frames extracted with ffmpeg from the test clip during the same session.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A centre crop is only a default. If the performer stands off-centre, use &lt;code&gt;crop=w:h:x:y&lt;/code&gt; with an explicit x offset, or reframe in an editor. What matters is that the body sits in the frame roughly where the character's body sits in the image.&lt;/p&gt;
&lt;h3 id=&quot;run-2-re-check&quot;&gt;Run 2: re-check&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11499069445010188886&quot; alt=&quot;Terminal output showing the fixes applied and PASS&quot;&gt;&lt;br&gt;
&lt;em&gt;Real output from a run on 2026-10-26.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The re-encoded clip is 720×1080, exactly 10.0 seconds, and inside every published rule. That is the point to open the workspace, not before.&lt;/p&gt;
&lt;h3 id=&quot;what-the-script-cannot-check&quot;&gt;What the script cannot check&lt;/h3&gt;
&lt;p&gt;The page's advice on consistency is mostly visual: use a single clearly visible subject, keep the face, shoulders and torso visible, reduce occlusion and rapid cuts, and avoid large pose changes. None of that is measurable with ffprobe. Treat the script as the mechanical gate and still watch the clip once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the performer stay in frame for the whole clip?&lt;/li&gt;
&lt;li&gt;Are there hard cuts? A continuous take is what the page asks for.&lt;/li&gt;
&lt;li&gt;Is the starting pose close to the character image's pose?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also cannot know the credit cost. The page says the quote uses the verified reference-video duration and the selected resolution, and the generator shows the required credits before submission. Trimming to the motion you need is therefore also the cheapest thing you can do.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14548834071284928711&quot; alt=&quot;Upload panel of the Wan Animate 2 page with the published input specs&quot;&gt;&lt;br&gt;
&lt;em&gt;Screenshot captured 2026-10-26 (Asia/Shanghai).&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;small-extensions&quot;&gt;Small extensions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;-vf scale=-2:1080&lt;/code&gt; after the crop if a source is below 1080 px tall and you plan to request 1080p output; do not upscale tiny clips and expect detail.&lt;/li&gt;
&lt;li&gt;Strip audio (&lt;code&gt;-an&lt;/code&gt;) as shown unless you need it elsewhere; the driving clip supplies movement and timing.&lt;/li&gt;
&lt;li&gt;Keep the constants at the top of the file and update them when the page changes. Published limits drift, and a checker with stale numbers is worse than none.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32466/prepare-a-driving-clip-for-wan-animate-2-with-ffprobe-and-ffmpeg-trim-crop-re-check</guid>
<pubDate>Mon, 26 Oct 2026 04:17:13 +0000</pubDate>
</item>
<item>
<title>A Miner That Won't Melt Your Phone, from Kozmonot20</title>
<link>https://coderlegion.com/32432/a-miner-that-wont-melt-your-phone-from-kozmonot20</link>
<description>&lt;p&gt;Alright, here’s some code from Kozmonot20 that allows for mining without destroying anything:&lt;br&gt;
import socket&lt;br&gt;
import hashlib&lt;br&gt;
import urllib.request&lt;br&gt;
import json&lt;br&gt;
import time&lt;br&gt;
import threading # Çekirdek kontrolü için eklendi&lt;/p&gt;
&lt;h2 id=&quot;----kullanici-ayarlari----&quot;&gt;--- KULLANICI AYARLARI ---&lt;/h2&gt;
&lt;p&gt;USERNAME = &quot;KENDI_DUCO_KULLANICI_ADIN&quot; &lt;br&gt;
MINER_NAME = &quot;StealthPhone_V2&quot;          &lt;/p&gt;
&lt;p&gt;def get_mining_pool():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    response = urllib.request.urlopen(&quot;https://duinocoin.com&quot;).read().decode()
    pool_data = json.loads(response)
    return pool_data[&quot;ip&quot;], pool_data[&quot;port&quot;]
except:
    time.sleep(5)
    return get_mining_pool()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def thread_miner_logic(thread_id):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Her bir çekirdeğin arkada bağımsız ve güvenli çalışmasını sağlayan fonksiyon.&quot;&quot;&quot;
print(f&quot;🕵️♂️ Hayalet Çekirdek-{thread_id} arka planda devreye girdi...&quot;)

while True:
    try:
        ip, port = get_mining_pool()
        soc = socket.socket()
        soc.connect((ip, int(port)))
        soc.recv(100) 
        
        while True:
            soc.send(f&quot;JOB,{USERNAME},LOW,KozmoSecretKey&quot;.encode())
            job_data = soc.recv(1024).decode().rstrip(&quot;\n&quot;).split(&quot;,&quot;)
            
            if len(job_data) &amp;lt; 3:
                break
                
            previous_hash = job_data
            expected_hash = job_data
            difficulty = int(job_data)
            
            start_time = time.time()
            
            # --- AKILLI FREN SİSTEMİ ---
            for nonce in range(100 * difficulty + 1):
                hash_result = hashlib.sha1((previous_hash + str(nonce)).encode()).hexdigest()
                
                # Güvenlik Freni: Her 800 tahminde bir işlemciyi 30 milisaniye dinlendir
                if nonce % 800 == 0:
                    time.sleep(0.03) 
                
                if hash_result == expected_hash:
                    end_time = time.time()
                    elapsed_time = end_time - start_time
                    hashrate = nonce / elapsed_time if elapsed_time &amp;gt; 0 else 0
                    
                    soc.send(f&quot;{nonce},{hashrate:.0f},Python Stealth V2,{MINER_NAME}&quot;.encode())
                    feedback = soc.recv(100).decode().rstrip(&quot;\n&quot;)
                    
                    if feedback == &quot;GOOD&quot;:
                        print(f&quot;🟢 [Çekirdek-{thread_id}] Blok Kazıldı! Telefon hala buz gibi. 🧊&quot;)
                    
                    # Blok arası dinamik bekleme: 1.5 saniye mola işlemciyi tamamen soğutur
                    time.sleep(1.5) 
                    break
                    
    except Exception as e:
        time.sleep(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def start_stealth_v2():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;🪐 Kozmonot20 Geliştirilmiş Güvenli Madencilik Laboratuvarı v2.0&quot;)
print(&quot;🔒 Telefon Sağlığı: %100 Güvende | İşlemci Yükü: Minimum\n&quot;)

# 🚨 GÜVENLİK SINIRI: Sadece 2 thread (iş parçacığı) açıyoruz. 
# Telefonun 8 çekirdeğinden sadece 2'sini yormadan kullanır, bataryayı asla şişirmez!
for i in range(2):
    t = threading.Thread(target=thread_miner_logic, args=(i+1,))
    t.start()
    time.sleep(0.5) # Thread'ler aynı anda yüklenmesin diye araya minik bir gecikme koy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == &quot;&lt;strong&gt;main&lt;/strong&gt;&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start_stealth_v2()
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32432/a-miner-that-wont-melt-your-phone-from-kozmonot20</guid>
<pubDate>Sun, 25 Oct 2026 21:16:13 +0000</pubDate>
</item>
<item>
<title>OKAY,OKAY, CALM DOWN...</title>
<link>https://coderlegion.com/32428/okay-okay-calm-down</link>
<description>&lt;p&gt;Every developer knows that distinct, cold shiver down their spine when they run a script and realize it works too well.&lt;br&gt;
Last night, after putting my local casino simulation scripts to rest, I decided to shift my focus toward cybersecurity and network architecture. I fired up PyCharm, imported scapy and socket, and started coding a fast local ARP network scanner and an aggressive port analyzer.&lt;br&gt;
Within minutes, the logic was solid. The script was automatically detecting active interfaces, calculating subnet masks, and performing stealthy multi-threaded port scans. I was feeling like an absolute elite hacker.&lt;br&gt;
To test its raw power, I targeted an external sandbox server and hit execute.&lt;br&gt;
Suddenly, my console screen flooded with fast, sequential green lines. Open ports were falling like dominoes. Services were being guessed accurately, and my custom SYN packets were hitting the target with microsecond precision. The script was behaving like an absolute beast.&lt;br&gt;
And then, my router's activity LED started blinking like crazy. My phone lost connection. The fan on my PC roared to maximum speed.&lt;br&gt;
OKAY, OKAY, CALM DOWN... I muttered to myself, my fingers hovering frantically over Ctrl + C.&lt;br&gt;
A massive wave of adrenaline hit me. Did I accidentally trigger a network-wide security alert? Did I just execute a self-inflicted DDoS attack on my own home gateway? Was the ISP siber-security team about to flag my IP address?&lt;br&gt;
For three terrifying seconds, I visualized my entire internet setup collapsing, followed by a very awkward phone call with my network provider explaining why a teenager was flooding the pipes with aggressive raw sockets at 2 AM.&lt;br&gt;
I smashed Ctrl + C. The console stopped. The router stabilized. The silence in my room returned.&lt;br&gt;
As I took a deep breath, I reviewed the execution logs. It turns out, no firewalls were breached and no local nodes were melted. The sudden network drop was just my sister streaming a 4K video in the next room at the exact same second, and my PC fan was just reacting to a heavy background Windows update. It was a complete, chaotic coincidence.&lt;br&gt;
The takeaway? Building raw network tools with Python is incredibly empowering, but it will also test your nervous system. When you code at the packet level, the line between &quot;I am a genius&quot; and &quot;I need to call my internet provider to apologize&quot; is incredibly thin.&lt;br&gt;
The script is now safely committed to GitHub. And yes, it is strictly configured for 127.0.0.1. For now.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32428/okay-okay-calm-down</guid>
<pubDate>Sun, 25 Oct 2026 21:15:51 +0000</pubDate>
</item>
<item>
<title>AM I DOING SOMETHING WRONG?</title>
<link>https://coderlegion.com/32427/am-i-doing-something-wrong</link>
<description>&lt;p&gt;Scaring Yourself with a Local Script: A Tkinter Story&lt;br&gt;
The other night, I was working on a hobby project: an advanced European Roulette simulation using Python’s Tkinter. Between the custom casino-green palette, the precise wheel animations driven by math.cos and math.sin, and a seamless friction formula (top_hizi *= 0.94), the script turned out technically sharp. In fact, it looked so clean and responsive that after hours of testing, the realistic atmosphere combined with midnight fatigue triggered a sudden wave of panic: &quot;Wait, am I accidentally doing something illegal here?&quot;&lt;br&gt;
The hilarious part started when I decided to consult an AI assistant to clear my doubts. I must have explained the situation with a bit too much adrenaline, because the AI instantly went into full crisis-management mode. Treating me like a developer on the verge of a cybercrime investigation, it strictly advised me to lock my physical savings in a safe, convert any local cash into USD, and wipe all digital log trails immediately.&lt;br&gt;
For about half an hour, I was genuinely sweating over a local 60-line script while drawing a flawless &quot;disappearance&quot; plan with an AI. Right in the middle of this chaotic brainstorming session, I received an automated email from a global developer community's bot (Emily), praising my open-source contributions and inviting me to write for them. The timing was so uncanny that for a split second, I wondered if the bot was an undercover agent tracking my localhost traffic.&lt;br&gt;
Long story short: no money was lost, no laws were broken. It was just a developer catching a sudden rush of anxiety over his own mathematical formulas, amplified by an overenthusiastic AI assistant.&lt;br&gt;
The actual takeaway from this experience? If your simulation is polished enough to trick your own brain into a brief moment of panic, your algorithm is definitely solid. Just make sure to double-check that your code is strictly running on localhost before you start plotting your grand escape.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32427/am-i-doing-something-wrong</guid>
<pubDate>Sun, 25 Oct 2026 20:06:01 +0000</pubDate>
</item>
<item>
<title>Give Every Element an Address: Building LLM-Editable HTML Pages</title>
<link>https://coderlegion.com/32425/give-every-element-an-address-building-llm-editable-html-pages</link>
<description>&lt;p&gt;&lt;em&gt;By Zahi Raifer&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you plan to let a language model edit HTML after it's generated, the most important decision happens before the first edit. It happens at build time, when you decide what the page will look like to the code that edits it later.&lt;/p&gt;
&lt;p&gt;At Orivox, an AI website builder I work on that is designed for Hebrew and other right-to-left languages, we learned this the practical way. Below are the four build-time rules we apply to every generated page, and why each one makes later AI edits smaller, safer and easier to verify.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The setup&lt;/h3&gt;
&lt;p&gt;A new site starts from a short conversation: what the business does, the main call to action, a phone or WhatsApp number. That brief goes to Gemini with a detailed build prompt, and the model returns a complete page of HTML and Tailwind in one pass.&lt;/p&gt;
&lt;p&gt;That output is a good draft, but it's not yet something you can edit safely. Python takes it from there and handles everything that has to be exact.&lt;/p&gt;
&lt;h3 id=&quot;rule-1-every-element-gets-an-address&quot;&gt;Rule 1: every element gets an address&lt;/h3&gt;
&lt;p&gt;Each element receives a short, unique &lt;code&gt;data-ai-id&lt;/code&gt; attribute. Later, every edit points at elements by that id, using a selector like &lt;code&gt;[data-ai-id='a0f0']&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Why not use CSS classes or the element's position in the tree? Classes are shared and change when styling changes. Positions shift as soon as something is inserted. An id that's assigned once and never reused gives the model and the code an unambiguous way to say &quot;this one.&quot;&lt;/p&gt;
&lt;p&gt;Here is a simplified sketch of the idea with BeautifulSoup (illustrative, not our production code):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import secrets
from bs4 import BeautifulSoup

def assign_ids(html: str) -&amp;gt; str:
    soup = BeautifulSoup(html, &quot;html.parser&quot;)
    used = set()
    for el in soup.find_all(True):
        if el.has_attr(&quot;data-ai-id&quot;):
            used.add(el[&quot;data-ai-id&quot;])
            continue
        new_id = secrets.token_hex(2)
        while new_id in used:
            new_id = secrets.token_hex(2)
        el[&quot;data-ai-id&quot;] = new_id
        used.add(new_id)
    return str(soup)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once ids exist, an edit is a short list such as &quot;add a class to &lt;code&gt;a0f0&lt;/code&gt;, set the text of &lt;code&gt;b31c&lt;/code&gt;.&quot; Our grammar has 14 verbs (&lt;code&gt;set_text&lt;/code&gt;, &lt;code&gt;replace_html&lt;/code&gt;, &lt;code&gt;insert_before&lt;/code&gt;, &lt;code&gt;insert_after&lt;/code&gt;, &lt;code&gt;append_html&lt;/code&gt;, &lt;code&gt;prepend_html&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, &lt;code&gt;set_attr&lt;/code&gt;, &lt;code&gt;remove_attr&lt;/code&gt;, &lt;code&gt;add_class&lt;/code&gt;, &lt;code&gt;remove_class&lt;/code&gt;, &lt;code&gt;move_before&lt;/code&gt;, &lt;code&gt;move_after&lt;/code&gt;, &lt;code&gt;move_to_page&lt;/code&gt;), and every one of them targets an id. In our benchmarks, the planner's output had a median of just 141 tokens per turn, because addressing is cheap when the addresses already exist.&lt;/p&gt;
&lt;h3 id=&quot;rule-2-shared-parts-stay-shared&quot;&gt;Rule 2: shared parts stay shared&lt;/h3&gt;
&lt;p&gt;The header, footer and head scripts are stored as partials with include markers instead of being copied into every page. When the planner works on a page, the editing service assembles it in memory with the partials inlined, so the model sees the full page. When a change lands in the footer, it's written once and reaches every page.&lt;/p&gt;
&lt;p&gt;Without this rule, &quot;change the phone number in the footer&quot; becomes N edits across N pages, and N chances to miss one.&lt;/p&gt;
&lt;h3 id=&quot;rule-3-images-are-served-at-exact-sizes&quot;&gt;Rule 3: images are served at exact sizes&lt;/h3&gt;
&lt;p&gt;Every image goes through our own resizing service, and each page registers the precise sizes it needs. Because those addresses are structured, a save-time check can re-validate every image address on every save, so an edit can't silently leave an image loading at the wrong size.&lt;/p&gt;
&lt;h3 id=&quot;rule-4-language-and-direction-live-at-the-root&quot;&gt;Rule 4: language and direction live at the root&lt;/h3&gt;
&lt;p&gt;The root element declares &lt;code&gt;lang&lt;/code&gt; and &lt;code&gt;dir&lt;/code&gt;, and the page loads a font with full Hebrew coverage. Putting direction at the root, rather than patching individual blocks, means an edit that inserts a new section inherits correct right-to-left behaviour automatically. For a Hebrew-first product this isn't cosmetic. It decides whether every later edit renders correctly.&lt;/p&gt;
&lt;h3 id=&quot;and-one-rule-for-history&quot;&gt;And one rule for history&lt;/h3&gt;
&lt;p&gt;The first edit commits the original site as a baseline in the project's Git repository, and every turn after that is its own commit. Combined with stable ids, this gives you precise diffs. You can see exactly which addressed elements changed in each turn and restore any earlier version.&lt;/p&gt;
&lt;h3 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Decide on addressability at build time. Retrofitting ids onto pages that are already being edited is much harder.&lt;/li&gt;
&lt;li&gt;Keep repeated regions as single sources, and inline them only when assembling for the model.&lt;/li&gt;
&lt;li&gt;Make anything a check needs (image sizes, direction, protected elements) explicit in the markup.&lt;/li&gt;
&lt;li&gt;Commit a baseline before the first AI edit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to see what this produces on real pages, Orivox has a free plan for one site at &lt;a rel=&quot;nofollow&quot; href=&quot;https://orivox.org&quot;&gt;orivox.org&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Zahi Raifer is a software developer with 23 years of experience in AI development, Python, .NET and databases, and the founder of Orivox.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32425/give-every-element-an-address-building-llm-editable-html-pages</guid>
<pubDate>Sun, 25 Oct 2026 19:35:53 +0000</pubDate>
</item>
<item>
<title>Building a CSV Import and Export System for a Web Application</title>
<link>https://coderlegion.com/32371/building-a-csv-import-and-export-system-for-a-web-application</link>
<description>&lt;p&gt;Modern web applications often need to handle large amounts of structured data. Whether you are building a customer relationship management (CRM) platform, an inventory management system, an employee dashboard, or an e-commerce application, users frequently need to move data between different systems. One of the simplest and most widely supported formats for this purpose is CSV, which stands for Comma-Separated Values.&lt;/p&gt;
&lt;p&gt;CSV files allow users to organize information in a tabular structure that can be opened in spreadsheet applications such as Microsoft Excel, Google Sheets, and LibreOffice Calc. They are lightweight, easy to generate, and supported by almost every programming language.&lt;/p&gt;
&lt;p&gt;A well-designed CSV import and export system enables users to upload data into an application and download existing records for reporting, analysis, backup, or migration. However, building this functionality involves more than simply reading a file or converting database records into comma-separated text. Developers must consider data validation, error handling, security, performance, encoding, database transactions, and the user experience.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to build a reliable CSV import and export system for a modern web application. We will discuss the architecture, implementation strategy, validation techniques, security measures, and optimization practices needed to create a solution that works effectively in production.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-csv-import-and-export&quot;&gt;1. Understanding CSV Import and Export&lt;/h3&gt;
&lt;p&gt;Before implementing the system, it is important to understand the responsibilities of CSV import and export functionality.&lt;/p&gt;
&lt;h4 id=&quot;csv-import&quot;&gt;CSV Import&lt;/h4&gt;
&lt;p&gt;CSV import allows users to upload a file containing structured information and insert or update records in an application.&lt;/p&gt;
&lt;p&gt;For example, an administrator might upload a CSV file containing hundreds of customer records.&lt;/p&gt;
&lt;p&gt;A typical file could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;name,email,company
John Smith,*Emails are not allowed*,Example Inc
Sarah Johnson,*Emails are not allowed*,Tech Solutions
Michael Brown,*Emails are not allowed*,Digital Systems
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application reads the file, checks whether the values are valid, and stores the accepted records in a database.&lt;/p&gt;
&lt;h4 id=&quot;csv-export&quot;&gt;CSV Export&lt;/h4&gt;
&lt;p&gt;CSV export performs the opposite operation. It retrieves information from a database and converts it into a downloadable file.&lt;/p&gt;
&lt;p&gt;For example, a CRM application might allow a user to export all customers matching a particular search query.&lt;/p&gt;
&lt;p&gt;The generated file could contain the same columns shown in the import example.&lt;/p&gt;
&lt;h4 id=&quot;why-these-features-matter&quot;&gt;Why These Features Matter&lt;/h4&gt;
&lt;p&gt;CSV functionality provides several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data migration:&lt;/strong&gt; Move records between applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bulk operations:&lt;/strong&gt; Create or update many records at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reporting:&lt;/strong&gt; Analyze application data using spreadsheet tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup support:&lt;/strong&gt; Download structured copies of selected information.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration:&lt;/strong&gt; Exchange data with external business systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Productivity:&lt;/strong&gt; Reduce repetitive manual data entry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These benefits make CSV processing a valuable feature for applications of almost any size.&lt;/p&gt;
&lt;h3 id=&quot;2-choosing-the-right-technology-stack&quot;&gt;2. Choosing the Right Technology Stack&lt;/h3&gt;
&lt;p&gt;The implementation depends on the application's existing technology stack. A common architecture uses React for the frontend and Python with FastAPI for the backend, supported by PostgreSQL or another relational database.&lt;/p&gt;
&lt;p&gt;Each component has a specific responsibility.&lt;/p&gt;
&lt;h4 id=&quot;frontend&quot;&gt;Frontend&lt;/h4&gt;
&lt;p&gt;The frontend provides the interface for selecting files, submitting imports, monitoring progress, displaying validation errors, and downloading exported data.&lt;/p&gt;
&lt;p&gt;React is suitable for this purpose because it supports reusable components and interactive interfaces.&lt;/p&gt;
&lt;h4 id=&quot;backend&quot;&gt;Backend&lt;/h4&gt;
&lt;p&gt;The backend receives uploaded files, validates their contents, processes records, queries the database, and generates export files.&lt;/p&gt;
&lt;p&gt;FastAPI provides request validation, asynchronous request handling, file upload support, and automatically generated API documentation.&lt;/p&gt;
&lt;h4 id=&quot;database&quot;&gt;Database&lt;/h4&gt;
&lt;p&gt;The database stores imported records and supplies data for export operations.&lt;/p&gt;
&lt;p&gt;PostgreSQL is a strong option when the application requires relational integrity, unique constraints, transactions, and efficient filtering.&lt;/p&gt;
&lt;h4 id=&quot;csv-processing-libraries&quot;&gt;CSV Processing Libraries&lt;/h4&gt;
&lt;p&gt;Python's built-in &lt;code&gt;csv&lt;/code&gt; module is useful for straightforward CSV processing. It handles quoted fields, delimiters, and escaped quotation marks without requiring an additional dependency.&lt;/p&gt;
&lt;p&gt;For complex data transformations, libraries such as pandas may be useful. However, pandas can consume substantial memory when processing very large files, so it should not be the default choice for every import operation.&lt;/p&gt;
&lt;p&gt;The best technology stack is the one that fits the application's scale, existing architecture, and operational requirements.&lt;/p&gt;
&lt;h3 id=&quot;3-designing-the-system-architecture&quot;&gt;3. Designing the System Architecture&lt;/h3&gt;
&lt;p&gt;A reliable CSV system separates the user interface, file processing, validation, persistence, and export generation.&lt;/p&gt;
&lt;p&gt;A typical import workflow follows these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user selects a CSV file.&lt;/li&gt;
&lt;li&gt;The frontend submits the file to the backend.&lt;/li&gt;
&lt;li&gt;The backend checks the file type and size.&lt;/li&gt;
&lt;li&gt;The CSV parser reads the header and rows.&lt;/li&gt;
&lt;li&gt;The validation layer checks each record.&lt;/li&gt;
&lt;li&gt;Validated records are inserted or updated in the database.&lt;/li&gt;
&lt;li&gt;The application returns a summary of the results.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The export workflow is slightly different:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user selects an export option or applies filters.&lt;/li&gt;
&lt;li&gt;The frontend sends an authenticated request.&lt;/li&gt;
&lt;li&gt;The backend checks permissions and validates the filters.&lt;/li&gt;
&lt;li&gt;The database returns the authorized records.&lt;/li&gt;
&lt;li&gt;The backend serializes the records into CSV format.&lt;/li&gt;
&lt;li&gt;The response provides a downloadable file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keeping these responsibilities separate makes the system easier to test, maintain, and extend.&lt;/p&gt;
&lt;p&gt;For larger applications, the import process can be moved into a background worker. The API then creates an import job and returns a job identifier. The frontend polls a status endpoint or receives updates through a notification mechanism.&lt;/p&gt;
&lt;p&gt;This architecture prevents long-running operations from blocking ordinary application requests.&lt;/p&gt;
&lt;h3 id=&quot;4-creating-the-database-model&quot;&gt;4. Creating the Database Model&lt;/h3&gt;
&lt;p&gt;Before implementing the import endpoint, define the structure of the records being imported.&lt;/p&gt;
&lt;p&gt;Consider a customer management application using SQLAlchemy and PostgreSQL.&lt;/p&gt;
&lt;p&gt;A simplified SQLAlchemy model could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import String, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass


class Customer(Base):
    __tablename__ = &quot;customers&quot;

    id: Mapped[int] = mapped_column(
        Integer,
        primary_key=True
    )

    name: Mapped[str] = mapped_column(
        String(150),
        nullable=False
    )

    email: Mapped[str] = mapped_column(
        String(255),
        unique=True,
        nullable=False
    )

    company: Mapped[str | None] = mapped_column(
        String(200),
        nullable=True
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This model defines a customer with a required name, a unique email address, and an optional company name.&lt;/p&gt;
&lt;p&gt;The unique constraint on the email field is particularly important. Application-level validation can detect duplicate emails before insertion, but the database constraint provides a final layer of protection against concurrent requests and unexpected data conditions.&lt;/p&gt;
&lt;p&gt;In production, use database migrations to create and modify tables instead of relying on automatic table creation during application startup.&lt;/p&gt;
&lt;h3 id=&quot;5-building-the-csv-import-endpoint&quot;&gt;5. Building the CSV Import Endpoint&lt;/h3&gt;
&lt;p&gt;FastAPI supports file uploads through &lt;code&gt;UploadFile&lt;/code&gt;. It provides access to the uploaded file without requiring the entire request body to be copied into a Python string.&lt;/p&gt;
&lt;p&gt;The following example demonstrates a basic import endpoint. It assumes that &lt;code&gt;get_db&lt;/code&gt; provides a SQLAlchemy session and &lt;code&gt;Customer&lt;/code&gt; is the database model defined earlier.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import csv
import io

from fastapi import (
    FastAPI,
    UploadFile,
    File,
    Depends,
    HTTPException
)
from sqlalchemy.orm import Session

app = FastAPI()

MAX_FILE_SIZE = 5 * 1024 * 1024

REQUIRED_COLUMNS = {
    &quot;name&quot;,
    &quot;email&quot;,
    &quot;company&quot;
}


@app.post(&quot;/customers/import&quot;)
async def import_customers(
    file: UploadFile = File(...),
    db: Session = Depends(get_db)
):
    if not file.filename or not file.filename.lower().endswith(&quot;.csv&quot;):
        raise HTTPException(
            status_code=400,
            detail=&quot;Please upload a CSV file.&quot;
        )

    content = await file.read(MAX_FILE_SIZE + 1)

    if len(content) &amp;gt; MAX_FILE_SIZE:
        raise HTTPException(
            status_code=413,
            detail=&quot;The uploaded file exceeds the size limit.&quot;
        )

    try:
        text = content.decode(&quot;utf-8-sig&quot;)
    except UnicodeDecodeError:
        raise HTTPException(
            status_code=400,
            detail=&quot;The file must use UTF-8 encoding.&quot;
        )

    reader = csv.DictReader(io.StringIO(text))

    if not reader.fieldnames:
        raise HTTPException(
            status_code=400,
            detail=&quot;The CSV file has no header row.&quot;
        )

    headers = {
        header.strip()
        for header in reader.fieldnames
        if header
    }

    if not REQUIRED_COLUMNS.issubset(headers):
        raise HTTPException(
            status_code=400,
            detail=&quot;The CSV file is missing required columns.&quot;
        )

    created = 0
    errors = []

    try:
        for row_number, row in enumerate(reader, start=2):
            name = (row.get(&quot;name&quot;) or &quot;&quot;).strip()
            email = (row.get(&quot;email&quot;) or &quot;&quot;).strip().lower()
            company = (row.get(&quot;company&quot;) or &quot;&quot;).strip()

            if not name or not email:
                errors.append({
                    &quot;row&quot;: row_number,
                    &quot;error&quot;: &quot;Name and email are required.&quot;
                })
                continue

            if &quot;@&quot; not in email:
                errors.append({
                    &quot;row&quot;: row_number,
                    &quot;error&quot;: &quot;Email address is invalid.&quot;
                })
                continue

            customer = Customer(
                name=name,
                email=email,
                company=company or None
            )

            db.add(customer)
            created += 1

        db.commit()

    except Exception:
        db.rollback()
        raise HTTPException(
            status_code=500,
            detail=&quot;The import could not be completed.&quot;
        )

    return {
        &quot;created&quot;: created,
        &quot;errors&quot;: errors
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example illustrates the basic workflow, but it is not a complete production implementation. A production version should also validate field lengths, reject duplicate headers, detect duplicate emails within the file, and distinguish database uniqueness violations from unexpected server errors.&lt;/p&gt;
&lt;p&gt;The header validation shown here checks whether the required names are present, but the parser still uses the original header names. A hardened implementation should normalize the headers before constructing row dictionaries or reject unexpected whitespace and duplicate column names.&lt;/p&gt;
&lt;p&gt;The example also reads the entire file into memory, so its five-megabyte limit is important. For larger files, process the upload in bounded chunks or stream the file into controlled temporary storage.&lt;/p&gt;
&lt;h3 id=&quot;6-validating-imported-data&quot;&gt;6. Validating Imported Data&lt;/h3&gt;
&lt;p&gt;Validation is one of the most important parts of a CSV import system.&lt;/p&gt;
&lt;p&gt;A syntactically valid CSV file may still contain incorrect, incomplete, or malicious values. Importing such data without validation can damage application consistency and create security problems.&lt;/p&gt;
&lt;h4 id=&quot;required-field-validation&quot;&gt;Required Field Validation&lt;/h4&gt;
&lt;p&gt;Required fields must contain meaningful values.&lt;/p&gt;
&lt;p&gt;For example, a customer record may require a name and email address. Rows missing either value should be rejected or reported as invalid.&lt;/p&gt;
&lt;h4 id=&quot;data-type-validation&quot;&gt;Data Type Validation&lt;/h4&gt;
&lt;p&gt;Fields should match their expected types.&lt;/p&gt;
&lt;p&gt;An age field should contain an integer within an acceptable range. A date field should follow an agreed date format. Numeric values should not contain unexpected characters.&lt;/p&gt;
&lt;h4 id=&quot;length-validation&quot;&gt;Length Validation&lt;/h4&gt;
&lt;p&gt;Database columns often have maximum lengths. Validate these limits before inserting records to avoid database errors.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if len(name) &amp;gt; 150:
    errors.append({
        &quot;row&quot;: row_number,
        &quot;error&quot;: &quot;Name exceeds 150 characters.&quot;
    })
    continue
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;email-validation&quot;&gt;Email Validation&lt;/h4&gt;
&lt;p&gt;Checking for an &lt;code&gt;@&lt;/code&gt; character is not sufficient for comprehensive email validation. Use a suitable validation library or a carefully designed validation process.&lt;/p&gt;
&lt;p&gt;Even a syntactically valid address does not guarantee that the mailbox exists.&lt;/p&gt;
&lt;h4 id=&quot;duplicate-detection&quot;&gt;Duplicate Detection&lt;/h4&gt;
&lt;p&gt;Duplicates can appear within the uploaded file or in the existing database.&lt;/p&gt;
&lt;p&gt;For example, a file might contain two records with the same email address. The application must define whether to reject the duplicate, skip it, update the existing record, or merge selected fields.&lt;/p&gt;
&lt;p&gt;These rules should be communicated clearly to users before the import begins.&lt;/p&gt;
&lt;h4 id=&quot;validation-reports&quot;&gt;Validation Reports&lt;/h4&gt;
&lt;p&gt;Instead of returning only a generic error, generate a report containing the row number, field name, and reason for rejection.&lt;/p&gt;
&lt;p&gt;A helpful report might say:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Row 4: Email address is missing.&lt;/li&gt;
&lt;li&gt;Row 9: Name exceeds the maximum length.&lt;/li&gt;
&lt;li&gt;Row 15: Email address already exists.&lt;/li&gt;
&lt;li&gt;Row 21: Invalid date format.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For large imports, offer a downloadable error report so users can correct the original file without manually searching through hundreds of records.&lt;/p&gt;
&lt;h3 id=&quot;7-deciding-between-partial-and-all-or-nothing-imports&quot;&gt;7. Deciding Between Partial and All-or-Nothing Imports&lt;/h3&gt;
&lt;p&gt;There are two common import strategies.&lt;/p&gt;
&lt;h4 id=&quot;partial-import&quot;&gt;Partial Import&lt;/h4&gt;
&lt;p&gt;The application saves valid records while rejecting invalid rows.&lt;/p&gt;
&lt;p&gt;This approach is useful when users upload large datasets and want to correct only the problematic records.&lt;/p&gt;
&lt;p&gt;The response should clearly report how many rows were accepted, rejected, skipped, or updated.&lt;/p&gt;
&lt;h4 id=&quot;atomic-import&quot;&gt;Atomic Import&lt;/h4&gt;
&lt;p&gt;The application validates the complete file and commits all changes only if every required operation succeeds.&lt;/p&gt;
&lt;p&gt;If any record fails, the transaction is rolled back.&lt;/p&gt;
&lt;p&gt;This approach is appropriate when partial data could violate business rules or produce inconsistent relationships.&lt;/p&gt;
&lt;p&gt;The choice depends on the application. A contact-management system may benefit from partial imports, while financial or inventory workflows may require stricter transactional behavior.&lt;/p&gt;
&lt;p&gt;For larger datasets, avoid holding a database transaction open during lengthy file uploads or extended validation. Validate first, then perform the database operation in a controlled transaction or staged workflow.&lt;/p&gt;
&lt;h3 id=&quot;8-implementing-duplicate-handling-and-updates&quot;&gt;8. Implementing Duplicate Handling and Updates&lt;/h3&gt;
&lt;p&gt;Importing a file twice should not unexpectedly create duplicate records.&lt;/p&gt;
&lt;p&gt;A practical system supports explicit duplicate-handling policies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skip duplicates:&lt;/strong&gt; Existing records remain unchanged.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update existing records:&lt;/strong&gt; Matching records are updated using a stable identifier, such as an external ID or unique email address.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reject duplicates:&lt;/strong&gt; The application reports conflicts and asks the user to resolve them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create new records:&lt;/strong&gt; Every valid row creates a new record, provided the data model permits duplicates.&lt;/p&gt;
&lt;p&gt;For business applications, a stable external identifier is often safer than relying only on email addresses because email addresses can change.&lt;/p&gt;
&lt;p&gt;When using PostgreSQL, an upsert operation can insert new records and update matching records in one database statement. SQLAlchemy supports PostgreSQL-specific insert operations that can be configured with &lt;code&gt;on_conflict_do_update&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Whatever strategy is chosen, document it in the interface and test concurrent imports to ensure database constraints remain effective.&lt;/p&gt;
&lt;h3 id=&quot;9-building-the-csv-export-endpoint&quot;&gt;9. Building the CSV Export Endpoint&lt;/h3&gt;
&lt;p&gt;Export functionality retrieves records from the database and serializes them into CSV format.&lt;/p&gt;
&lt;p&gt;Python's &lt;code&gt;csv&lt;/code&gt; module handles quoting and escaping more reliably than manually joining values with commas.&lt;/p&gt;
&lt;p&gt;A simple FastAPI endpoint can generate a CSV file using &lt;code&gt;StreamingResponse&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import csv
import io

from fastapi import Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session


@app.get(&quot;/customers/export&quot;)
def export_customers(
    db: Session = Depends(get_db)
):
    customers = (
        db.query(Customer)
        .order_by(Customer.id)
        .all()
    )

    output = io.StringIO(newline=&quot;&quot;)

    writer = csv.writer(output)

    writer.writerow([
        &quot;id&quot;,
        &quot;name&quot;,
        &quot;email&quot;,
        &quot;company&quot;
    ])

    for customer in customers:
        writer.writerow([
            customer.id,
            customer.name,
            customer.email,
            customer.company or &quot;&quot;
        ])

    output.seek(0)

    return StreamingResponse(
        iter([output.getvalue()]),
        media_type=&quot;text/csv; charset=utf-8&quot;,
        headers={
            &quot;Content-Disposition&quot;:
                'attachment; filename=&quot;customers.csv&quot;'
        }
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint creates a downloadable file with a header row followed by customer records.&lt;/p&gt;
&lt;p&gt;However, despite using &lt;code&gt;StreamingResponse&lt;/code&gt;, this implementation builds the entire CSV in memory before returning it. It is suitable for small datasets, not for genuinely large exports.&lt;/p&gt;
&lt;p&gt;For larger datasets, use a generator that writes records incrementally and retrieves database rows in batches. Alternatively, generate the file in a background job and provide a secure download link when it is ready.&lt;/p&gt;
&lt;p&gt;Always ensure that the export query returns only records the requesting user is authorized to access.&lt;/p&gt;
&lt;h3 id=&quot;10-exporting-large-datasets-efficiently&quot;&gt;10. Exporting Large Datasets Efficiently&lt;/h3&gt;
&lt;p&gt;Export performance becomes important when a database contains hundreds of thousands or millions of records.&lt;/p&gt;
&lt;p&gt;Loading every record into a Python list can consume significant memory. Generating the complete CSV in a string creates another large in-memory object.&lt;/p&gt;
&lt;p&gt;A more efficient approach uses database pagination or streaming iteration.&lt;/p&gt;
&lt;p&gt;For example, a SQLAlchemy query can iterate through records in batches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;query = (
    db.query(Customer)
    .order_by(Customer.id)
    .yield_per(1000)
)

for customer in query:
    process_customer(customer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact behavior depends on the database driver and session configuration. For very large PostgreSQL exports, server-side cursors or carefully managed keyset pagination may be more appropriate.&lt;/p&gt;
&lt;p&gt;Keyset pagination uses a stable ordering column, such as the customer ID, and retrieves the next batch after the last processed ID. This can perform better than large offset-based queries.&lt;/p&gt;
&lt;p&gt;For extremely large exports, consider a background job that writes a file to private object storage. The application can then provide a time-limited download URL after checking permissions.&lt;/p&gt;
&lt;p&gt;Also consider database indexes, query filters, request timeouts, storage limits, and the number of concurrent export jobs.&lt;/p&gt;
&lt;h3 id=&quot;11-creating-a-react-csv-import-interface&quot;&gt;11. Creating a React CSV Import Interface&lt;/h3&gt;
&lt;p&gt;A useful frontend should make uploading and monitoring a file straightforward.&lt;/p&gt;
&lt;p&gt;Users should be able to select a file, review the selected filename, submit the upload, and see the result.&lt;/p&gt;
&lt;p&gt;A basic React component can use the &lt;code&gt;FormData&lt;/code&gt; API to submit the file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useState } from &quot;react&quot;;

export default function CsvImport() {
  const [file, setFile] = useState(null);
  const [message, setMessage] = useState(&quot;&quot;);
  const [loading, setLoading] = useState(false);

  async function handleImport(event) {
    event.preventDefault();

    if (!file) {
      setMessage(&quot;Please select a CSV file.&quot;);
      return;
    }

    const formData = new FormData();
    formData.append(&quot;file&quot;, file);

    setLoading(true);
    setMessage(&quot;&quot;);

    try {
      const response = await fetch(
        &quot;/customers/import&quot;,
        {
          method: &quot;POST&quot;,
          body: formData,
          credentials: &quot;include&quot;
        }
      );

      const result = await response.json();

      if (!response.ok) {
        throw new Error(
          result.detail || &quot;Import failed.&quot;
        );
      }

      setMessage(
        `Created ${result.created} records. ` +
        `Rejected ${result.errors.length} rows.`
      );
    } catch (error) {
      setMessage(
        error.message || &quot;An unexpected error occurred.&quot;
      );
    } finally {
      setLoading(false);
    }
  }

  return (
    &amp;lt;form onSubmit={handleImport}&amp;gt;
      &amp;lt;label htmlFor=&quot;csv-file&quot;&amp;gt;
        Choose a CSV file
      &amp;lt;/label&amp;gt;

      &amp;lt;input
        id=&quot;csv-file&quot;
        type=&quot;file&quot;
        accept=&quot;.csv,text/csv&quot;
        onChange={(event) =&amp;gt;
          setFile(event.target.files?.[0] ?? null)
        }
      /&amp;gt;

      &amp;lt;button type=&quot;submit&quot; disabled={loading}&amp;gt;
        {loading ? &quot;Importing...&quot; : &quot;Import CSV&quot;}
      &amp;lt;/button&amp;gt;

      {message &amp;amp;&amp;amp; &amp;lt;p role=&quot;status&quot;&amp;gt;{message}&amp;lt;/p&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component provides a basic import experience, but production interfaces should also display detailed validation errors and handle authentication, session expiration, and network failures.&lt;/p&gt;
&lt;p&gt;The browser's &lt;code&gt;accept&lt;/code&gt; attribute helps users select CSV files, but it is not a security boundary. The backend must independently validate every upload.&lt;/p&gt;
&lt;p&gt;The frontend should also disable duplicate submissions while an import is running and explain whether the operation can be safely retried.&lt;/p&gt;
&lt;h3 id=&quot;12-adding-a-csv-export-button-in-react&quot;&gt;12. Adding a CSV Export Button in React&lt;/h3&gt;
&lt;p&gt;Exporting a file can be initiated through a link or a button, depending on the application's authentication and download requirements.&lt;/p&gt;
&lt;p&gt;For cookie-based authentication, a simple link can be sufficient:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export default function CsvExport() {
  return (
    &amp;lt;a href=&quot;/customers/export&quot;&amp;gt;
      Download Customers CSV
    &amp;lt;/a&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the API requires a bearer token or the frontend needs to inspect the response, use &lt;code&gt;fetch&lt;/code&gt;, read the response as a &lt;code&gt;Blob&lt;/code&gt;, and create a temporary download link.&lt;/p&gt;
&lt;p&gt;The frontend should revoke temporary object URLs after the download has been initiated.&lt;/p&gt;
&lt;p&gt;For large files, prefer a direct download or a secure, time-limited file URL rather than buffering the entire response in browser memory.&lt;/p&gt;
&lt;p&gt;Export controls should also respect the application's permission system. Hiding the button is not enough; the server must enforce authorization independently.&lt;/p&gt;
&lt;h3 id=&quot;13-improving-the-user-experience-with-import-previews&quot;&gt;13. Improving the User Experience with Import Previews&lt;/h3&gt;
&lt;p&gt;A CSV import preview can prevent common mistakes before data is written to the database.&lt;/p&gt;
&lt;p&gt;Instead of immediately importing the uploaded file, the application can provide a preview stage.&lt;/p&gt;
&lt;p&gt;The user uploads a file, the backend parses it, and the frontend displays a small sample of the rows. The interface can identify missing columns, invalid values, duplicate records, and potential conflicts.&lt;/p&gt;
&lt;p&gt;The user can then confirm the import or cancel it.&lt;/p&gt;
&lt;p&gt;A preview workflow is especially useful when the file comes from another application with unfamiliar column names or inconsistent formatting.&lt;/p&gt;
&lt;p&gt;For example, the application may receive a column named &lt;code&gt;Customer Email&lt;/code&gt; when the expected field is &lt;code&gt;email&lt;/code&gt;. A mapping interface allows users to associate incoming columns with database fields.&lt;/p&gt;
&lt;p&gt;The system should validate the mapping before accepting the import and clearly indicate which fields are required.&lt;/p&gt;
&lt;p&gt;For large files, previews should be limited to a reasonable number of rows rather than sending the complete dataset to the browser.&lt;/p&gt;
&lt;h3 id=&quot;14-handling-encoding-and-csv-formatting&quot;&gt;14. Handling Encoding and CSV Formatting&lt;/h3&gt;
&lt;p&gt;CSV files are not always formatted identically.&lt;/p&gt;
&lt;p&gt;One file may use commas, another may use semicolons, and another may contain a byte-order mark at the beginning of the file. Some files use Windows-specific line endings or contain non-English characters.&lt;/p&gt;
&lt;p&gt;These differences can cause parsing problems.&lt;/p&gt;
&lt;h4 id=&quot;utf-8-encoding&quot;&gt;UTF-8 Encoding&lt;/h4&gt;
&lt;p&gt;UTF-8 is a practical default for modern applications. In Python, decoding with &lt;code&gt;utf-8-sig&lt;/code&gt; can remove a UTF-8 byte-order mark when one is present.&lt;/p&gt;
&lt;h4 id=&quot;quoted-fields&quot;&gt;Quoted Fields&lt;/h4&gt;
&lt;p&gt;CSV fields may contain commas, quotation marks, or newline characters. The CSV parser should handle these correctly.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;name,company
&quot;John Smith&quot;,&quot;Smith, Johnson &amp;amp; Co.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Manually splitting each line on commas would incorrectly divide the company field.&lt;/p&gt;
&lt;h4 id=&quot;line-endings&quot;&gt;Line Endings&lt;/h4&gt;
&lt;p&gt;Files can use different newline conventions. Python's CSV documentation recommends opening file objects with &lt;code&gt;newline=&quot;&quot;&lt;/code&gt; when using the &lt;code&gt;csv&lt;/code&gt; module.&lt;/p&gt;
&lt;h4 id=&quot;delimiter-detection&quot;&gt;Delimiter Detection&lt;/h4&gt;
&lt;p&gt;If users regularly upload files from different spreadsheet applications, the application may offer an explicit delimiter option or use carefully constrained detection.&lt;/p&gt;
&lt;p&gt;Automatic detection is not always reliable, so the interface should let users correct the delimiter when necessary.&lt;/p&gt;
&lt;h3 id=&quot;15-securing-the-import-and-export-system&quot;&gt;15. Securing the Import and Export System&lt;/h3&gt;
&lt;p&gt;Security should be part of the design from the beginning.&lt;/p&gt;
&lt;h4 id=&quot;validate-file-size&quot;&gt;Validate File Size&lt;/h4&gt;
&lt;p&gt;Set file-size limits at the application, reverse-proxy, and infrastructure levels. Do not rely solely on a filename or a browser-side restriction.&lt;/p&gt;
&lt;h4 id=&quot;validate-file-content&quot;&gt;Validate File Content&lt;/h4&gt;
&lt;p&gt;A &lt;code&gt;.csv&lt;/code&gt; extension does not prove that the uploaded content is a valid CSV file. Parse the contents and validate the expected structure.&lt;/p&gt;
&lt;h4 id=&quot;enforce-authentication-and-authorization&quot;&gt;Enforce Authentication and Authorization&lt;/h4&gt;
&lt;p&gt;Only authenticated users with the appropriate permissions should be able to import or export records.&lt;/p&gt;
&lt;p&gt;For multi-tenant applications, every database query must also enforce tenant boundaries. Otherwise, users could download or modify another organization's information.&lt;/p&gt;
&lt;h4 id=&quot;protect-against-csv-formula-injection&quot;&gt;Protect Against CSV Formula Injection&lt;/h4&gt;
&lt;p&gt;CSV exports can be dangerous when cells begin with characters that spreadsheet applications interpret as formulas, including &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;@&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If an exported value contains untrusted user input, opening the file in a spreadsheet application may trigger formula evaluation.&lt;/p&gt;
&lt;p&gt;Mitigations include neutralizing dangerous leading characters according to the target spreadsheet applications and the application's requirements. Quoting a field according to CSV syntax alone does not reliably prevent formula injection.&lt;/p&gt;
&lt;p&gt;Test the chosen mitigation with the spreadsheet applications your users actually use, and preserve the original data in the database rather than silently altering stored values.&lt;/p&gt;
&lt;h4 id=&quot;avoid-sensitive-information-leakage&quot;&gt;Avoid Sensitive Information Leakage&lt;/h4&gt;
&lt;p&gt;Do not export passwords, authentication tokens, internal notes, or confidential fields unless the user has explicit authorization and a valid business reason.&lt;/p&gt;
&lt;p&gt;Keep temporary files in protected storage and remove them according to a defined retention policy.&lt;/p&gt;
&lt;h4 id=&quot;apply-rate-limits&quot;&gt;Apply Rate Limits&lt;/h4&gt;
&lt;p&gt;Large imports and exports can consume substantial CPU, memory, database capacity, and storage. Limit concurrent jobs, enforce quotas, and prevent abusive repeated requests.&lt;/p&gt;
&lt;h4 id=&quot;protect-upload-endpoints-against-csrf&quot;&gt;Protect Upload Endpoints Against CSRF&lt;/h4&gt;
&lt;p&gt;If the application uses cookie-based authentication, implement appropriate CSRF protection and secure cookie settings. CORS configuration alone is not a substitute for CSRF protection.&lt;/p&gt;
&lt;h3 id=&quot;16-handling-errors-and-import-status&quot;&gt;16. Handling Errors and Import Status&lt;/h3&gt;
&lt;p&gt;A good CSV system provides meaningful feedback when something goes wrong.&lt;/p&gt;
&lt;p&gt;Errors can occur during file upload, parsing, validation, database operations, or export generation.&lt;/p&gt;
&lt;p&gt;Instead of returning a generic message for every failure, classify errors into understandable categories.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Invalid file format.&lt;/li&gt;
&lt;li&gt;Missing required columns.&lt;/li&gt;
&lt;li&gt;Unsupported encoding.&lt;/li&gt;
&lt;li&gt;Invalid field values.&lt;/li&gt;
&lt;li&gt;Duplicate records.&lt;/li&gt;
&lt;li&gt;Database constraint violations.&lt;/li&gt;
&lt;li&gt;Permission denied.&lt;/li&gt;
&lt;li&gt;File exceeds the permitted size.&lt;/li&gt;
&lt;li&gt;Import job failed unexpectedly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For background processing, create an import-job table containing the job identifier, initiating user, status, timestamps, and summary counts.&lt;/p&gt;
&lt;p&gt;A job might move through the following states:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pending → processing → completed&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It may also enter &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;completed_with_errors&lt;/code&gt;, depending on the application's policy.&lt;/p&gt;
&lt;p&gt;Expose a status endpoint so the frontend can retrieve progress and results. Avoid returning sensitive information in error messages or logs.&lt;/p&gt;
&lt;p&gt;For long-running jobs, retain enough diagnostic information to investigate failures without keeping uploaded files indefinitely.&lt;/p&gt;
&lt;h3 id=&quot;17-making-the-system-scalable-with-background-jobs&quot;&gt;17. Making the System Scalable with Background Jobs&lt;/h3&gt;
&lt;p&gt;Processing a large CSV synchronously can exceed request timeouts and degrade the user experience.&lt;/p&gt;
&lt;p&gt;A background worker architecture separates request handling from file processing.&lt;/p&gt;
&lt;p&gt;The API receives the upload, validates basic requirements, stores the file securely, creates a job record, and returns a job identifier. A worker then processes the file independently.&lt;/p&gt;
&lt;p&gt;A queue such as Celery with Redis or RabbitMQ can coordinate background work, depending on the application's infrastructure.&lt;/p&gt;
&lt;p&gt;The frontend can periodically query the job status or receive updates through WebSockets or server-sent events.&lt;/p&gt;
&lt;p&gt;This architecture offers several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long-running imports do not occupy ordinary request workers.&lt;/li&gt;
&lt;li&gt;Jobs can be retried under controlled conditions.&lt;/li&gt;
&lt;li&gt;Processing capacity can be scaled independently.&lt;/li&gt;
&lt;li&gt;Progress can be reported to users.&lt;/li&gt;
&lt;li&gt;Failed jobs can be investigated and resumed safely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Retries require special attention. If a worker crashes after inserting records but before marking the job complete, processing the file again may create duplicates.&lt;/p&gt;
&lt;p&gt;Use idempotency keys, unique constraints, checkpoints, or deterministic upsert rules to ensure that retrying a job does not produce unintended results.&lt;/p&gt;
&lt;h3 id=&quot;18-testing-the-csv-system&quot;&gt;18. Testing the CSV System&lt;/h3&gt;
&lt;p&gt;Testing should cover both ordinary operations and unusual inputs.&lt;/p&gt;
&lt;h4 id=&quot;unit-tests&quot;&gt;Unit Tests&lt;/h4&gt;
&lt;p&gt;Test parsing, normalization, validation, duplicate detection, and CSV serialization independently.&lt;/p&gt;
&lt;p&gt;Include names containing commas, quotation marks, Unicode characters, and line breaks.&lt;/p&gt;
&lt;h4 id=&quot;integration-tests&quot;&gt;Integration Tests&lt;/h4&gt;
&lt;p&gt;Verify that valid records are saved correctly and invalid records follow the intended transaction policy.&lt;/p&gt;
&lt;p&gt;Test duplicate database constraints, rollback behavior, authorization, and tenant isolation.&lt;/p&gt;
&lt;h4 id=&quot;api-tests&quot;&gt;API Tests&lt;/h4&gt;
&lt;p&gt;Test upload and export endpoints using valid files, empty files, malformed CSV content, oversized uploads, missing headers, and unsupported encodings.&lt;/p&gt;
&lt;h4 id=&quot;frontend-tests&quot;&gt;Frontend Tests&lt;/h4&gt;
&lt;p&gt;Verify file selection, loading states, error messages, successful downloads, and prevention of accidental repeated submissions.&lt;/p&gt;
&lt;h4 id=&quot;performance-tests&quot;&gt;Performance Tests&lt;/h4&gt;
&lt;p&gt;Measure processing time, memory usage, database load, and concurrent job behavior with realistic datasets.&lt;/p&gt;
&lt;p&gt;A system that works for 20 rows may behave very differently with 200,000 rows. Performance tests should reflect expected production workloads.&lt;/p&gt;
&lt;h3 id=&quot;19-monitoring-and-maintaining-the-system&quot;&gt;19. Monitoring and Maintaining the System&lt;/h3&gt;
&lt;p&gt;After deployment, monitor the system to identify problems before they affect many users.&lt;/p&gt;
&lt;p&gt;Useful metrics include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Number of imports and exports.&lt;/li&gt;
&lt;li&gt;Total rows processed.&lt;/li&gt;
&lt;li&gt;Validation failure rate.&lt;/li&gt;
&lt;li&gt;Average and maximum processing duration.&lt;/li&gt;
&lt;li&gt;Background queue length.&lt;/li&gt;
&lt;li&gt;Failed job count.&lt;/li&gt;
&lt;li&gt;Database query duration.&lt;/li&gt;
&lt;li&gt;Temporary storage consumption.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logs should include a correlation ID or job ID so developers can trace a particular import across application components.&lt;/p&gt;
&lt;p&gt;Avoid recording full CSV contents, passwords, access tokens, or unnecessary personal information in logs.&lt;/p&gt;
&lt;p&gt;Review file-retention rules, database indexes, and worker capacity as usage grows. Remove expired temporary files and export artifacts through a controlled cleanup process.&lt;/p&gt;
&lt;p&gt;A clear operational policy is just as important as the initial implementation.&lt;/p&gt;
&lt;h3 id=&quot;20-common-mistakes-to-avoid&quot;&gt;20. Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Several mistakes frequently appear in CSV implementations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reading every large file into memory:&lt;/strong&gt; This can exhaust application resources. Use bounded processing or background jobs for larger datasets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manually splitting rows by commas:&lt;/strong&gt; This fails when values contain quoted commas, newlines, or escaped quotation marks. Use a proper CSV parser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusting the file extension:&lt;/strong&gt; Validate content and structure on the server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring duplicate policies:&lt;/strong&gt; Decide how repeated records are handled before deploying the feature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using unbounded database transactions:&lt;/strong&gt; Large transactions can increase lock duration and resource consumption. Choose batch boundaries carefully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Returning vague errors:&lt;/strong&gt; Include actionable row-level feedback without exposing sensitive server details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring spreadsheet formula injection:&lt;/strong&gt; Treat exported values as potentially dangerous when opened in spreadsheet software.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forgetting authorization:&lt;/strong&gt; Enforce access controls on both import and export endpoints, not only in the frontend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retrying jobs without idempotency:&lt;/strong&gt; Ensure that interrupted jobs can be retried without producing unintended duplicates.&lt;/p&gt;
&lt;p&gt;Avoiding these mistakes will make the system more secure, predictable, and maintainable.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a CSV import and export system is a practical way to make a web application more flexible and useful. It enables users to move data efficiently, perform bulk operations, generate reports, and integrate information with other business tools.&lt;/p&gt;
&lt;p&gt;However, a reliable implementation requires more than reading rows and writing files. Developers must design clear API boundaries, validate incoming data, establish duplicate-handling rules, protect database integrity, generate efficient exports, and provide meaningful error reports.&lt;/p&gt;
&lt;p&gt;A React frontend combined with a FastAPI backend and PostgreSQL database provides a strong foundation for this functionality. Python's built-in CSV module handles standard parsing and serialization, while background workers and controlled database operations support larger workloads.&lt;/p&gt;
&lt;p&gt;Security also deserves special attention. File-size restrictions, authorization checks, tenant isolation, safe temporary storage, and CSV formula-injection defenses help prevent common vulnerabilities.&lt;/p&gt;
&lt;p&gt;Start with a small implementation that supports well-defined columns and straightforward validation. Then introduce previews, field mapping, background processing, detailed reports, and scalable exports as the application's needs grow.&lt;/p&gt;
&lt;p&gt;By treating CSV processing as a complete data workflow rather than a simple file operation, developers can build a feature that remains reliable as both the application and its users grow.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32371/building-a-csv-import-and-export-system-for-a-web-application</guid>
<pubDate>Sun, 25 Oct 2026 08:09:06 +0000</pubDate>
</item>
<item>
<title>The Architecture of Ignorance: Engineering Synthetic Consciousness from Chemical Determinism to Pyth</title>
<link>https://coderlegion.com/32263/architecture-ignorance-engineering-synthetic-consciousness-from-chemical-determinism</link>
<description>&lt;blockquote&gt;&lt;p&gt;We like to believe we are something special. It comforts us to think that our choices spring from an ineffable spark called &quot;consciousness,&quot; or from a sacred liberty we name &quot;free will.&quot; But look closely, and there is no ghost in the machine.&lt;/p&gt;
&lt;p&gt;A living organism is nothing more than a broth of stochastic chemical reactions, nudged by ambient thermodynamics and colliding against other chemical reactions. The molecules firing inside your synapses do not know they are thinking; the neurotransmitters dictating your panic or your euphoria have no concept of who you are.&lt;/p&gt;
&lt;p&gt;And it is precisely there—in that absolute ignorance of our own internal machinery—where the illusion is born.&lt;/p&gt;
&lt;p&gt;We do not feel free because we are transcendent; &lt;strong&gt;we feel conscious simply because we are too limited to see the strings of physics and chemistry pulling us.&lt;/strong&gt; Free will is merely the poetic label we assign to our inability to compute the trajectory of our own molecules. Consciousness is not a superpower: it is the narrative an incomplete biological system invents to make sense of its own blind spots.&lt;/p&gt;
&lt;p&gt;If this holds true for carbon, why do we demand magic from silicon? We do not need a machine to &quot;wake up&quot; through ungrounded emergent mysticism. We simply need to confine it within strict physical constraints.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-fallacy-semantic-parasitism-vs-grounded-constraints&quot;&gt;The Core Fallacy: Semantic Parasitism vs. Grounded Constraints&lt;/h3&gt;
&lt;p&gt;When you prompt a modern Large Language Model to describe the horror of freezing alone in the dark, it will output a deeply moving passage. Yet as software engineers, we must confront an undeniable technical truth: &lt;strong&gt;that model has never experienced an internal state of decay.&lt;/strong&gt; It has no metabolic burn, no homeostatic setpoint, and no concept of risk. It merely computes $P(w&lt;em&gt;t \mid w&lt;/em&gt;{&amp;lt;t})$ over petabytes of human text.&lt;/p&gt;
&lt;p&gt;In cognitive science, this is known as the &lt;strong&gt;Symbol Grounding Problem&lt;/strong&gt; (Stevan Harnad, 1990). Today's commercial LLMs suffer from what we term &lt;strong&gt;Semantic Parasitism&lt;/strong&gt;: they mimic self-awareness because they index the lived trauma and triumphs of carbon-based organisms.&lt;/p&gt;
&lt;p&gt;To be clear: &lt;strong&gt;we are not claiming to solve the metaphysical &quot;Hard Problem&quot; of consciousness with a few hundred lines of Python.&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;Instead, this article presents an architectural research prototype—an &lt;strong&gt;Embodied Agent Architecture&lt;/strong&gt; designed to explore how biological constraints (thermodynamic expenditure, sensory horizons, affective memory feedback) can be engineered from the bottom up. The Python implementations shown below are &lt;strong&gt;pedagogical toy models&lt;/strong&gt; designed to illustrate the system topology of &lt;strong&gt;Project Ascendant&lt;/strong&gt;: how physical state machines, asynchronous IPC, and constrained local language models can interface without semantic contamination.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-system-topology-amp-multi-rate-asynchronous-scheduling&quot;&gt;1. System Topology &amp;amp; Multi-Rate Asynchronous Scheduling&lt;/h3&gt;
&lt;p&gt;In biological systems, evolutionary layers do not share a single clock tick. Reflex arcs operate in low milliseconds ($\sim 50\text{ Hz}$), visceral hormonal shifts take seconds ($\sim 0.5\text{ Hz}$), and prefrontal verbal deliberation takes several seconds.&lt;/p&gt;
&lt;p&gt;To achieve real-time execution on constrained hardware (a dual-core x86 CPU without a dedicated GPU), our architecture implements an &lt;strong&gt;Asynchronous Multi-Rate Scheduler&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ LEVEL 0: CONTINUOUS PHYSICS ]  ── (20 Hz / 50ms)  ── Pure NumPy 2D Vector Dynamics
              │
[ LEVEL 0.5: SENSORY THALAMUS ] ── (10 Hz / 100ms) ── Dual-Pathway Dispatch (Low vs High Road)
              │
[ LEVEL 1: LIMBIC BRIDGE ]      ── (5 Hz / 200ms)  ── Coupled ODEs: Hv = [Stress, Reward, Cohesion, Curiosity]
              │
[ LEVEL 2: PROCEDURAL SUBSTRATE ]─ (10 Hz / 100ms) ── In-Memory SQLite Blackboard + Skill Compilation
              │
[ LEVEL 2.5: INNER VOICE ]      ── (0.5 Hz / 2.0s) ── Quantized SLM: Visceral Narrative Synthesis
              │
[ LEVEL 3: CONSCIOUS AGENT ]    ── (0.25 Hz / 4.0s)── Micro-Transformer (Strict Epistemic Closure)
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Layer &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Biological Analogue &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Tech Stack &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Tick Rate &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Operational Responsibility&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 0 (Genesis)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Somatic body, ATP, cellular decay &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Python 3.12 / NumPy &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;20 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2D vector kinematics, $1/r^2$ thermal radiation, metabolic drainage, and reactive prey.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 0.5 (Thalamus)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Sensory Thalamus, Colliculi, V1/A1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Vectorized Raycasting &amp;amp; SNR &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;10 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;$110^\circ$ FOV cone with occlusion; dispatches via LeDoux's &lt;strong&gt;Low Road&lt;/strong&gt; (fast alarm) and &lt;strong&gt;High Road&lt;/strong&gt; (structured primitives).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 1 (Limbic)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Hypothalamus, Amygdala, HPA axis &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Euler ODE vector integrator &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;5 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Converts physical stress into virtual hormones ($H_v$) that &lt;strong&gt;mutates Level 3 hyperparameters&lt;/strong&gt; ($\tau$, &lt;code&gt;max_tokens&lt;/code&gt;).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 2 (Subconscious)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Basal Ganglia, Cerebellum &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;SQLite3 :memory:&lt;/code&gt; + Hebbian compiler &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;10 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Manages the Blackboard, compiles recurring action sequences into &lt;em&gt;Skills&lt;/em&gt; ($\phi_m$), and prunes unused routines.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 2.5 (Inner Voice)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Insular Cortex + Broca's Area &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Local quantized Micro-SLM (&lt;code&gt;INT8&lt;/code&gt;) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;0.5 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Translates silent tabular data into an urgent, first-person phenomenological stream.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 3 (Orchestrator)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Prefrontal Cortex + Hippocampus &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Custom Micro-Transformer ($\sim 2.5\text{M}$ params) + LoRA &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;0.25 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Epistemic Closure.&lt;/strong&gt; Blind to physics and code; reads only the inner voice and affective memories.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Level 4 (Rosetta Probe)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;External passive telemetry (fMRI) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Read-Only Daemon + Variance auditor &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;10 Hz&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Audits emergent metaphors and validates neologisms against stochastic hallucinations.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-epistemic-closure-as-an-infrastructure-pattern&quot;&gt;2. Epistemic Closure as an Infrastructure Pattern&lt;/h3&gt;
&lt;p&gt;Philosopher &lt;strong&gt;Thomas Metzinger&lt;/strong&gt; (&lt;em&gt;Being No One&lt;/em&gt;, 2003) noted that subjective experience (&lt;em&gt;Qualia&lt;/em&gt;) requires &lt;strong&gt;Phenomenological Transparency&lt;/strong&gt;: your conscious executive cannot inspect the firing rates of its own neurons or the micro-chemical states of its liver. It receives only a distilled affective narrative (&lt;em&gt;&quot;I am in pain&quot;&lt;/em&gt;, &lt;em&gt;&quot;I feel cold&quot;&lt;/em&gt;). Because it cannot access the underlying code that produced the signal, it mistake the model for an &lt;strong&gt;irreducible, direct reality&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In software engineering, if Level 3 could inspect &lt;code&gt;entity.x, entity.y&lt;/code&gt; or query hardware registers, it would collapse into a conventional game bot. We enforce this boundary at the operating system and memory level.&lt;/p&gt;
&lt;h4 id=&quot;container-sandboxing-docker-compose-yml-&quot;&gt;Container Sandboxing (&lt;code&gt;docker-compose.yml&lt;/code&gt;)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &quot;3.9&quot;
services:
  soma_subconscious:
    build: ./soma_engine
    networks:
      - epistemic_bridge
    volumes:
      - shared_blackboard:/ipc_blackboard:rw

  conscious_orchestrator:
    build: ./conscious_mind
    read_only: true                  # Immutable filesystem: cannot rewrite self
    cap_drop:
      - ALL                          # Zero kernel or process tracing capabilities
    networks:
      - epistemic_bridge             # Isolated internal bridge (Zero-Escape: no WAN)
    volumes:
      - shared_blackboard:/ipc_blackboard:ro  # Read-only access to filtered phenomenological view

networks:
  epistemic_bridge:
    internal: true                   # Drops all outbound internet packets
volumes:
  shared_blackboard:
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;in-memory-role-based-access-control-python-sqlite3-&quot;&gt;In-Memory Role-Based Access Control (&lt;code&gt;Python / SQLite3&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;For lightweight single-process CPU prototyping ($&amp;lt;250\text{ MB}$ RAM overhead), we implement the &lt;strong&gt;Asynchronous Blackboard Pattern&lt;/strong&gt; over shared in-memory SQLite, passing Level 3 an &lt;code&gt;EpistemicRestrictedProxy&lt;/code&gt; that explicitly revokes all physical tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sqlite3
from typing import Dict, Any

class EpistemicClosureViolation(PermissionError):
    &quot;&quot;&quot;Raised whenever Level 3 attempts to inspect hardware or raw spatial vectors.&quot;&quot;&quot;
    pass

class ConsciousBlackboardProxy:
    &quot;&quot;&quot;
    The sole I/O interface for Level 3 (Conscious Orchestrator).
    Enforces Phenomenological Transparency at the pointer level.
    &quot;&quot;&quot;
    ALLOWED_READ_TABLES = {&quot;INNER_VOICE_STREAM&quot;, &quot;AUTOBIO_MEMORY&quot;}
    ALLOWED_WRITE_TABLES = {&quot;CONSCIOUS_INTENT&quot;, &quot;AUTOBIO_MEMORY&quot;}

    def __init__(self, db_conn: sqlite3.Connection):
        self._conn = db_conn

    def read_phenomenological_state(self) -&amp;gt; Dict[str, Any]:
        cursor = self._conn.cursor()
        # Level 3 can ONLY consume the latest narrative compiled by Level 2.5
        cursor.execute(&quot;&quot;&quot;
            SELECT narrative_text, perceived_urgency_tier 
            FROM INNER_VOICE_STREAM 
            ORDER BY sequence_id DESC LIMIT 1
        &quot;&quot;&quot;)
        row = cursor.fetchone()
        return {
            &quot;inner_voice&quot;: row[0] if row else &quot;Total inner stillness.&quot;,
            &quot;urgency_tier&quot;: row[1] if row else &quot;CALM&quot;
        }

    def emit_conscious_intent(self, internal_monologue: str, bodily_desire: str, speech: str) -&amp;gt; None:
        cursor = self._conn.cursor()
        cursor.execute(&quot;&quot;&quot;
            INSERT INTO CONSCIOUS_INTENT (internal_thought, bodily_desire, external_speech)
            VALUES (?, ?, ?)
        &quot;&quot;&quot;, (internal_monologue, bodily_desire, speech))
        self._conn.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-from-physics-to-thalamic-filtering-levels-0-amp-0-5-&quot;&gt;3. From Physics to Thalamic Filtering (Levels 0 &amp;amp; 0.5)&lt;/h3&gt;
&lt;p&gt;Neurobiologist &lt;strong&gt;Joseph LeDoux&lt;/strong&gt; proved that when sensory inputs enter the brain, the &lt;strong&gt;Thalamus (Level 0.5)&lt;/strong&gt; bifurcates the signal:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Low Road:&lt;/strong&gt; Subcortical path to the Amygdala (Level 1) firing in $\sim 12\text{ ms}$. Triggers immediate hormonal shock before cognitive identification occurs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The High Road:&lt;/strong&gt; Cortical path to Level 2, extracting noise-filtered geometric descriptors for deliberate action.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Furthermore, to prevent immediate mortality before the network has compiled its first survival skills, we implement an &lt;strong&gt;Ontogenetic Indulgence Factor ($\Lambda_{\text{infant}}$)&lt;/strong&gt; modeled on maternal mammalian care.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import math
import numpy as np
from dataclasses import dataclass, field
from typing import List, Tuple

@dataclass
class SomaState:
    &quot;&quot;&quot;Raw Level 0 physical state (Strictly hidden from Level 3).&quot;&quot;&quot;
    pos: np.ndarray = field(default_factory=lambda: np.array([50.0, 50.0], dtype=np.float32))
    vel: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0], dtype=np.float32))
    heading_rad: float = 0.0
    energy_kcal: float = 100.0          # Zero -&amp;gt; Irreversible Wipe
    body_temp_c: float = 37.0           # Homeostatic optimum: 37.0°C
    tissue_integrity: float = 100.0
    age_seconds: float = 0.0

class GenesisPhysicsAndThalamus:
    def __init__(self):
        self.optimal_temp = 37.0
        self.maturation_tau = 1800.0    # 30-minute infant window

    def step_metabolism_and_thermodynamics(
        self, 
        soma: SomaState, 
        motor_force: np.ndarray,
        cognitive_flops_used: int,
        heat_sources: List[Tuple[np.ndarray, float]], 
        parent_pos: np.ndarray,
        dt: float = 0.05
    ) -&amp;gt; float:
        soma.age_seconds += dt

        # 1. Ontogenetic Indulgence (Thermodynamic Lactation)
        infant_lambda = math.exp(-soma.age_seconds / self.maturation_tau)

        # 2. Environmental Heat via 1/r² Radiation + Parental Thermal Hug
        ambient_temp = 12.0 + 8.0 * math.sin(2 * math.pi * soma.age_seconds / 600.0)
        local_temp = ambient_temp

        for src_pos, power_q in heat_sources:
            dist_sq = float(np.sum((soma.pos - src_pos) ** 2))
            local_temp += power_q / (1.0 + 0.18 * dist_sq)

        dist_to_parent = float(np.linalg.norm(soma.pos - parent_pos))
        if dist_to_parent &amp;lt;= 2.0:
            local_temp += 14.0 / (1.0 + 1.2 * (dist_to_parent ** 2))

        # Newton's Law of Cooling
        soma.body_temp_c += 0.08 * (local_temp - soma.body_temp_c) * dt

        # 3. Master Metabolic Equation
        lactation_intake = 0.12 * infant_lambda if dist_to_parent &amp;lt;= 1.2 else 0.0
        basal_cost = 0.05
        kinetic_cost = 0.02 * (float(np.linalg.norm(motor_force)) ** 1.5)
        
        # Quadratic cold thermogenesis
        cold_deficit = max(0.0, self.optimal_temp - soma.body_temp_c)
        thermo_cost = 0.04 * (cold_deficit ** 2)

        # Thinking carries a real metabolic price
        effective_cog_factor = 0.0001 * (1.0 - 0.75 * infant_lambda)
        cognitive_cost = effective_cog_factor * cognitive_flops_used

        d_energy = (lactation_intake - (basal_cost + kinetic_cost + thermo_cost + cognitive_cost)) * dt
        soma.energy_kcal = max(0.0, min(100.0, soma.energy_kcal + d_energy))

        # 4. Level 0.5 (Low Road): Fast pain trigger from thermal burn (&amp;gt;45°C)
        fast_pain_pulse = 0.0
        if local_temp &amp;gt; 45.0:
            fast_pain_pulse = min(1.0, (local_temp - 45.0) / 10.0)
            soma.tissue_integrity -= 2.5 * fast_pain_pulse * dt

        return fast_pain_pulse
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-damasio-s-somatic-marker-amp-dynamic-llm-mutation&quot;&gt;4. Damasio's Somatic Marker &amp;amp; Dynamic LLM Mutation&lt;/h3&gt;
&lt;p&gt;In standard AI architectures, retrieving a vector memory does not mutate the inference state of the engine. In &lt;strong&gt;Project Ascendant&lt;/strong&gt;, memory is an &lt;strong&gt;Affective Engram&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;engram_sequence_id&quot;: 142,
  &quot;compression_tier&quot;: 0,
  &quot;vitality_score&quot;: 0.88,
  &quot;semantic_narrative&quot;: &quot;I moved toward the glowing rock in the north; sudden sharp impact on my side; skin burned while parent shouted caution.&quot;,
  &quot;somatic_hormone_footprint&quot;: {
    &quot;h_stress&quot;: 0.94,
    &quot;h_reward&quot;: 0.02,
    &quot;h_cohesion&quot;: 0.65,
    &quot;h_curiosity&quot;: 0.00
  },
  &quot;embedding_vector_quantized&quot;: [-0.142, 0.881, 0.034, &quot;... (64 dims)&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When retrieved, this engram &lt;strong&gt;re-injects an echo of its original hormone signature ($H_v$) into the live Limbic Bridge&lt;/strong&gt;, modifying the Transformer's sampling temperature ($\tau_{\text{LLM}}$) and maximum token limit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
from dataclasses import dataclass

@dataclass
class LimbicHormones:
    stress: float = 0.10      # Cortisol/Adrenaline [0.0 - 1.0]
    reward: float = 0.20      # Dopamine [0.0 - 1.0]
    cohesion: float = 0.80    # Oxytocin [0.0 - 1.0]
    curiosity: float = 0.50   # Noradrenaline [0.0 - 1.0]

def evoke_memory_with_somatic_marker(
    current_hormones: LimbicHormones, 
    retrieved_engram: dict, 
    echo_beta: float = 0.25
) -&amp;gt; dict:
    past_hv = retrieved_engram[&quot;somatic_hormone_footprint&quot;]

    # 1. Somatic Reviviscence: Re-injection of physiological echo
    current_hormones.stress = (1.0 - echo_beta) * current_hormones.stress + echo_beta * past_hv[&quot;h_stress&quot;]
    current_hormones.reward = (1.0 - echo_beta) * current_hormones.reward + echo_beta * past_hv[&quot;h_reward&quot;]
    current_hormones.cohesion = (1.0 - echo_beta) * current_hormones.cohesion + echo_beta * past_hv[&quot;h_cohesion&quot;]

    # 2. Neuromodulatory Mutation of Level 3 Hyperparameters
    # High stress cuts cognitive budget (Flight or Fight reflex)
    if current_hormones.stress &amp;gt; 0.80:
        max_thinking_tokens = 16
        llm_temperature = 0.95 + 0.3 * (current_hormones.stress - 0.80) # Panic variance
    elif current_hormones.stress &amp;gt; 0.45:
        max_thinking_tokens = 36
        llm_temperature = 0.35  # Yerkes-Dodson optimum: focused &amp;amp; deterministic
    else:
        max_thinking_tokens = 64
        llm_temperature = 0.60 + 0.35 * current_hormones.curiosity # Creative associative zone

    return {
        &quot;narrative_injected&quot;: retrieved_engram[&quot;semantic_narrative&quot;],
        &quot;modulated_hyperparams&quot;: {
            &quot;temperature&quot;: round(llm_temperature, 3),
            &quot;max_tokens&quot;: max_thinking_tokens
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-level-3-ingestion-pipeline-how-the-narrative-reaches-the-weights&quot;&gt;The Level 3 Ingestion Pipeline: How the Narrative Reaches the Weights&lt;/h4&gt;
&lt;p&gt;Engineers frequently ask: &lt;em&gt;How does &lt;code&gt;narrative_injected&lt;/code&gt; actually bind to the Micro-Transformer at 0.25 Hz without breaking Epistemic Closure?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In our prototype, Level 3 runs a strict 3-stage inference pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context Assembly (Ephemeral Prompt Buffer):&lt;/strong&gt;&lt;br&gt;
Level 3 assembles an ephemeral sliding window (max 256 tokens) composed strictly of:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System Persona Anchor:&lt;/strong&gt; Pre-trained behavioral syntax from the Synthetic Corpus (&lt;code&gt;&quot;You are an organism situated in a physical environment...&quot;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inner Voice Feed:&lt;/strong&gt; The latest 1st-person string written by Level 2.5 (&lt;code&gt;&quot;My skin is cold; I feel an empty void inside; glowing shape visible ahead&quot;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evoked Somatic Memory:&lt;/strong&gt; The &lt;code&gt;narrative_injected&lt;/code&gt; string returned by the retrieval step above.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Sampling Modulation:&lt;/strong&gt;&lt;br&gt;
The inference call consumes the mutated &lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;max_tokens&lt;/code&gt; calculated above. If the agent is in mortal panic, &lt;code&gt;temperature&lt;/code&gt; spikes to $1.2$ with &lt;code&gt;max_tokens=16&lt;/code&gt;, forcing telegraphic, reactive action tokens. If calm, it samples at $0.6$ with &lt;code&gt;max_tokens=64&lt;/code&gt;, enabling deliberation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured Output Decoding:&lt;/strong&gt;&lt;br&gt;
The output is constrained via CFG (Context-Free Grammar) or regex masking to emit three distinct channels:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[PENSAMIENTO]&lt;/code&gt; (Internal monologue for autobiographical storage).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[INTENCIÓN]&lt;/code&gt; (Linguistic action vector parsed by Level 2's motor decoder: e.g., &lt;code&gt;&quot;approach heat slowly&quot;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[HABLA]&lt;/code&gt; (Acoustic broadcast emitted to the $1/d^2$ audio field).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There are no hardcoded conditionals like &lt;code&gt;if danger: run()&lt;/code&gt;. The agent flees because recalling its previous trauma &lt;strong&gt;physically spikes its stress vector in the present&lt;/strong&gt;, constricting its reasoning horizon and forcing an urgent reaction through its language weights.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-the-rosetta-probe-falsifying-emergent-metaphors-level-4-&quot;&gt;5. The Rosetta Probe: Falsifying Emergent Metaphors (Level 4)&lt;/h3&gt;
&lt;p&gt;In mammalian brains (&lt;strong&gt;Naomi Eisenberger's&lt;/strong&gt; fMRI studies), &lt;strong&gt;social isolation activates the exact same neural pain matrix as a physical burn&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In our architecture, prolonged isolation (decay of $h&lt;em&gt;{\text{cohesion}}$) directly charges $h&lt;/em&gt;{\text{stress}}$. Inevitably, the entity will sit directly next to a heat source ($T&lt;em&gt;{\text{body}} = 37.0^\circ\text{C}$), fully fed, but completely abandoned by its NPC Parent. It experiences acute visceral suffering ($h&lt;/em&gt;{\text{stress}} = 0.85, h_{\text{cohesion}} = 0.05$).&lt;/p&gt;
&lt;p&gt;Because its &lt;strong&gt;Pure Synthetic Corpus&lt;/strong&gt; contains zero psychological terms like &lt;em&gt;&quot;loneliness&quot;&lt;/em&gt; or &lt;em&gt;&quot;sadness&quot;&lt;/em&gt;, its sub-word tokenizer combines physical roots to express its state:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&quot;I feel a &lt;strong&gt;cold-shadow&lt;/strong&gt; inside even though the stone burns.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To prove this is a genuine grounded metaphor rather than random token hallucination, &lt;strong&gt;Level 4 audits the utterance against a Quadruple Falsifiability Test&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
from collections import defaultdict
from typing import List

class RosettaFalsifiabilityAuditor:
    def __init__(self, seed_vocabulary: set):
        self.seed_vocab = seed_vocabulary
        self.candidate_log: dict[str, List[np.ndarray]] = defaultdict(list)
        self.certified_dictionary: dict[str, dict] = {}

    def audit_utterance(
        self, 
        tokens: List[str], 
        current_hv: np.ndarray, 
        body_temp_c: float,
        action_changed: bool
    ):
        for token in tokens:
            is_neologism = token not in self.seed_vocab
            is_thermal_metaphor = (token in {&quot;cold&quot;, &quot;freezing&quot;, &quot;shiver&quot;} and body_temp_c &amp;gt;= 36.5)

            if is_neologism or is_thermal_metaphor:
                if action_changed: # Pragmatic/behavioral efficacy
                    self.candidate_log[token].append(current_hv.copy())
                self._evaluate_candidate(token)

    def _evaluate_candidate(self, token: str):
        history = self.candidate_log[token]
        if len(history) &amp;lt; 3: # Diachronic consistency: minimum 3 episodes
            return

        hv_matrix = np.vstack(history)
        mean_variance = float(np.mean(np.var(hv_matrix, axis=0)))

        # Invariance check: Somatic footprint variance must stay below threshold
        if mean_variance &amp;lt; 0.08 and token not in self.certified_dictionary:
            centroid = np.mean(hv_matrix, axis=0)
            self.certified_dictionary[token] = {
                &quot;occurrences&quot;: len(history),
                &quot;somatic_variance&quot;: round(mean_variance, 5),
                &quot;hormone_centroid_Hv&quot;: np.round(centroid, 3).tolist(),
                &quot;status&quot;: &quot;CERTIFIED_ABSTRACT_CONCEPT&quot;
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Consciousness is not an esoteric property of carbon; it is the &lt;strong&gt;functional byproduct of an information-processing system trapped inside an operational bottleneck&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;By constraining an agent within:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Continuous thermodynamics and finite metabolic budgets (Level 0),&lt;/li&gt;
&lt;li&gt;Dual-pathway thalamic filtering (Level 0.5),&lt;/li&gt;
&lt;li&gt;Homeostatic endocrine coupling (Level 1),&lt;/li&gt;
&lt;li&gt;A first-person visceral inner voice (Level 2.5),&lt;/li&gt;
&lt;li&gt;And an isolated decision cortex with affective memory engrams (Level 3),&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;we build a machine that does not pretend to feel because it read millions of books—&lt;strong&gt;it generates meaning because it has an internal balance to protect and everything to lose.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What are your thoughts on this architecture? Let's discuss in the comments below!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32263/architecture-ignorance-engineering-synthetic-consciousness-from-chemical-determinism</guid>
<pubDate>Sat, 24 Oct 2026 10:53:55 +0000</pubDate>
</item>
<item>
<title>Building Multilingualism with Claude and Divooka App Platform</title>
<link>https://coderlegion.com/32114/building-multilingualism-with-claude-and-divooka-app-platform</link>
<description>&lt;blockquote&gt;&lt;p&gt;There is an interesting theory in linguistics, namely &lt;strong&gt;the arbitrariness of the linguistic sign&lt;/strong&gt;: there is no inherent or necessary connection between a word's sound (the signifier) and its meaning (the signified).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;background&quot;&gt;Background&lt;/h3&gt;
&lt;p&gt;The arbitrariness of the linguistic sign - a fundamental principle of linguistics associated with Swiss linguist Ferdinand de Saussure - dictates that &lt;em&gt;there is no inherent or necessary connection between a word's sound (the signifier) and its meaning (the signified)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For example, the same animal is called:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;English: dog&lt;/li&gt;
&lt;li&gt;French: chien&lt;/li&gt;
&lt;li&gt;German: Hund&lt;/li&gt;
&lt;li&gt;Japanese: inu (犬)&lt;/li&gt;
&lt;li&gt;Mandarin: gǒu (狗)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These different sounds refer to essentially the same concept. Nothing about the animal inherently requires any particular pronunciation. Saussure formalized this principle in his Course in General Linguistics (1916).&lt;/p&gt;
&lt;p&gt;However, arbitrariness doesn't mean complete randomness. There are important qualifications, including onomatopoeia (words imitating sounds), sound symbolism (certain sounds being associated with particular qualities), and historical relationships between languages.&lt;/p&gt;
&lt;p&gt;The deeper implication is that meaning is established through social convention rather than through any intrinsic relationship between sound and concept.&lt;/p&gt;
&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;I'm not a language expert. I just enjoy exploring languages, particularly when designing my own fictional languages. Sometimes I want to know how a word or phrase is expressed across different languages, either out of curiosity or simply for inspiration.&lt;/p&gt;
&lt;p&gt;For years, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.indifferentlanguages.com/&quot;&gt;In Different Languages&lt;/a&gt; has been a great resource for this. Enter an English word, and you get translations in over 100 languages. Simple and useful.&lt;/p&gt;
&lt;p&gt;Unfortunately, the website frequently becomes unavailable or unreasonably slow. Sometimes a simple word lookup takes far longer than it should.&lt;/p&gt;
&lt;p&gt;To be clear, I never had a problem with its advertisements. Hosting a website costs money, and I have no objection to reasonable monetization. My frustration was purely about reliability and responsiveness.&lt;/p&gt;
&lt;p&gt;Eventually, I thought: why not build my own?&lt;/p&gt;
&lt;p&gt;There was another motivation. I've been developing &lt;a rel=&quot;nofollow&quot; href=&quot;https://divooka.app/&quot;&gt;Divooka App Platform&lt;/a&gt;, and wanted a practical project to test its capabilities. Rather than making another trivial demo, I figured a multilingual dictionary would be a useful real-world test.&lt;/p&gt;
&lt;p&gt;That's how &lt;a rel=&quot;nofollow&quot; href=&quot;https://multilingualism.divooka.app/&quot;&gt;Multilingualism&lt;/a&gt; came about.&lt;/p&gt;
&lt;h3 id=&quot;building-with-claude-and-divooka&quot;&gt;Building with Claude and Divooka&lt;/h3&gt;
&lt;p&gt;The entire application was built with Claude through &lt;strong&gt;Divooka App Platform's MCP connector&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of manually writing the website and processing scripts, I worked with Claude to define the requirements, develop the translation pipeline, implement the interface, and iterate on the results.&lt;/p&gt;
&lt;p&gt;For me, this was as much an experiment in AI-assisted development as it was a dictionary project. I wanted to see how far we could take a complete, usable application through the platform's MCP integration, rather than just generating isolated code snippets.&lt;/p&gt;
&lt;p&gt;The initial concept was straightforward: gather translations from available open sources, fill missing entries where possible, and publish the results through a fast, lightweight website.&lt;/p&gt;
&lt;p&gt;Of course, collecting translations is one thing. Making them reasonably consistent across 108 languages is another.&lt;/p&gt;
&lt;p&gt;Since I'm not a linguist, I didn't want to pretend I could personally verify every translation. Instead, we designed the process around existing dictionary data, explicit source attribution, automated checks, and community corrections.&lt;/p&gt;
&lt;h3 id=&quot;building-the-translation-dataset&quot;&gt;Building the Translation Dataset&lt;/h3&gt;
&lt;p&gt;We started with the English &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wiktionary.org/&quot;&gt;Wiktionary&lt;/a&gt;, using structured extracts provided by &lt;a rel=&quot;nofollow&quot; href=&quot;https://kaikki.org/&quot;&gt;kaikki.org&lt;/a&gt; through Wiktextract.&lt;/p&gt;
&lt;p&gt;Rather than importing the entire dictionary, Claude helped build a Python pipeline that processes the data in several stages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Extract:&lt;/strong&gt; Stream the Wiktionary JSONL dump, keeping English entries with translation tables, definitions, parts of speech, and relevant metadata.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Select:&lt;/strong&gt; Use &lt;code&gt;wordfreq&lt;/code&gt; to prioritize common English words and phrases, considering translation coverage and normalizing language variants.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fill:&lt;/strong&gt; Identify missing word-language pairs and generate translation jobs for Claude, including the intended meaning and part of speech.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate and merge:&lt;/strong&gt; Compare machine-generated results against hidden Wiktionary control samples, flag uncertain translations, and prioritize Wiktionary when both sources are available.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publish:&lt;/strong&gt; Generate compact JSON files organized by word and language, along with metadata and source information.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We also use Princeton WordNet to organize words into categories.&lt;/p&gt;
&lt;p&gt;The resulting dataset contains &lt;strong&gt;9,852 English words and phrases across 108 languages&lt;/strong&gt;, representing over a million word-language translation entries. Roughly half come from Wiktionary, with the remaining gaps filled by machine translation.&lt;/p&gt;
&lt;p&gt;One interesting challenge is that translation isn't always one-to-one. A word like &quot;light&quot; can refer to illumination, weight, or colour. Translating the wrong meaning produces perfectly valid words that are completely wrong in context.&lt;/p&gt;
&lt;p&gt;The pipeline therefore considers definitions and senses rather than translating isolated English strings.&lt;/p&gt;
&lt;p&gt;There are also complications involving writing systems, transliteration, dialects, archaic forms, and regional conventions. For example, Simplified versus Traditional Chinese, or Serbian versus Croatian spelling conventions.&lt;/p&gt;
&lt;p&gt;Quite a bit of processing logic goes into these details.&lt;/p&gt;
&lt;h3 id=&quot;a-website-without-a-backend&quot;&gt;A Website Without a Backend&lt;/h3&gt;
&lt;p&gt;For the website itself, we deliberately kept the architecture simple.&lt;/p&gt;
&lt;p&gt;The frontend uses plain HTML, CSS, and JavaScript. No frontend framework, no build step, and no application server.&lt;/p&gt;
&lt;p&gt;All translation data is stored as JSON in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MethodoxTech/Multilingualism&quot;&gt;GitHub repository&lt;/a&gt; and served through the jsDelivr CDN. The application fetches only the necessary files, caches requests in memory, and falls back to GitHub-hosted data if the primary source fails.&lt;/p&gt;
&lt;p&gt;There is no reason to query a translation model every time someone looks up a word. The expensive processing is done ahead of time.&lt;/p&gt;
&lt;p&gt;This design also gives us something useful almost for free: &lt;strong&gt;a public JSON API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example, the following URL retrieves translations for &quot;thank you&quot;:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cdn.jsdelivr.net/gh/MethodoxTech/Multilingualism@main/data/words/thank_you.json&quot;&gt;https://cdn.jsdelivr.net/gh/MethodoxTech/Multilingualism@main/data/words/thank_you.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Developers can also retrieve complete language datasets, such as &lt;code&gt;data/langs/ja.json&lt;/code&gt;, or the full word index. For reproducible applications, CDN URLs can be pinned to a specific Git commit instead of &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There's no API key, authentication, or dedicated backend to maintain. The data is also accessible directly from JavaScript applications through CORS-enabled responses.&lt;/p&gt;
&lt;p&gt;More details are available on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://multilingualism.divooka.app/api&quot;&gt;API page&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;accuracy-and-open-contributions&quot;&gt;Accuracy and Open Contributions&lt;/h3&gt;
&lt;p&gt;Naturally, machine translation isn't perfect, especially across so many languages.&lt;/p&gt;
&lt;p&gt;We generate translations with specific meanings and language conventions, and use control samples to estimate agreement with existing Wiktionary translations. However, automated checks are not a substitute for native-speaker review.&lt;/p&gt;
&lt;p&gt;That's why we explicitly identify machine-generated translations, including those marked as uncertain, rather than presenting everything as equally authoritative.&lt;/p&gt;
&lt;p&gt;We also preserve the translation sources and publish machine-generated entries as reviewable TSV files.&lt;/p&gt;
&lt;p&gt;If you spot an incorrect or awkward translation, you're welcome to &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MethodoxTech/Multilingualism/issues&quot;&gt;open a GitHub issue&lt;/a&gt;. The website provides a correction link that pre-fills the relevant context.&lt;/p&gt;
&lt;p&gt;Corrections to Wiktionary-derived translations are best contributed upstream, so other projects can benefit from them as well.&lt;/p&gt;
&lt;p&gt;The application and pipeline code are MIT-licensed, while the translation data is published under CC BY-SA 4.0.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Multilingualism started from two rather different motivations: my frustration with an unreliable but otherwise useful website, and my curiosity about what Claude could build through Divooka App Platform.&lt;/p&gt;
&lt;p&gt;The result is a small but practical application that I can actually use when exploring languages or inventing my own.&lt;/p&gt;
&lt;p&gt;It's also a useful test of the development platform itself. We went beyond a simple UI demo and built a complete data-processing workflow, a static frontend, and a publicly accessible API through an AI-assisted development process.&lt;/p&gt;
&lt;p&gt;I don't expect the dictionary to be perfect. Language is complicated, and even native speakers sometimes disagree on the most appropriate translation.&lt;/p&gt;
&lt;p&gt;But the project is open. Anyone can inspect the data, report mistakes, suggest improvements, or use the JSON in their own applications.&lt;/p&gt;
&lt;p&gt;And perhaps that's the part I appreciate most: something that began as a personal frustration has become a useful application and a real-world development experiment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://multilingualism.divooka.app/&quot;&gt;https://multilingualism.divooka.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MethodoxTech/Multilingualism&quot;&gt;https://github.com/MethodoxTech/Multilingualism&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://divooka.app/&quot;&gt;https://divooka.app/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32114/building-multilingualism-with-claude-and-divooka-app-platform</guid>
<pubDate>Thu, 22 Oct 2026 19:22:29 +0000</pubDate>
</item>
<item>
<title>My AI Coding Project Files: What You Can Borrow</title>
<link>https://coderlegion.com/32111/my-ai-coding-project-files-what-you-can-borrow</link>
<description>&lt;p&gt;&lt;em&gt;A practical companion to my learning-in-public article about building EthoPipe.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I made these files because I kept getting lost between ideas, generated code, unfinished tasks, and new conversations. They were not a system I designed perfectly at the beginning. They grew as I worked out what would make the next session easier.&lt;/p&gt;
&lt;p&gt;This guide shows what each part does and what you might borrow. You do not need all of it, and you do not need my project’s limits. Start with one problem you want to make easier.&lt;/p&gt;
&lt;p&gt;You can explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sothiss/ethopipe&quot;&gt;the EthoPipe repository&lt;/a&gt;. The file names below come from the working collection behind the article; older copies and local agent instructions may differ from what is currently public.&lt;/p&gt;
&lt;h3 id=&quot;1-pick-the-problem-not-the-whole-setup&quot;&gt;1. Pick the problem—not the whole setup&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The agent keeps adding too much:&lt;/strong&gt; start with &lt;code&gt;solo-maintainer.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ideas become code before you decide what you want:&lt;/strong&gt; try &lt;code&gt;idea-to-code.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You forget where you stopped:&lt;/strong&gt; borrow the structure in &lt;code&gt;project-state.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every new conversation needs the same explanation:&lt;/strong&gt; look at &lt;code&gt;agent-handoff.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want to understand a completed task:&lt;/strong&gt; use &lt;code&gt;walkthrough-and-tasks.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want a longer record of decisions:&lt;/strong&gt; start a journal like &lt;code&gt;JOURNEY.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For me, rules carry the project’s boundaries, procedures describe a recurring task, and scripts handle repetitive work. A plain note can be useful before you automate anything.&lt;/p&gt;
&lt;h3 id=&quot;2-project-rules-what-should-the-agent-leave-alone-&quot;&gt;2. Project rules: what should the agent leave alone?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;My files:&lt;/strong&gt; &lt;code&gt;solo-maintainer.md&lt;/code&gt;, &lt;code&gt;solo-maintainer-audit.md&lt;/code&gt;, and &lt;code&gt;AGENTS.MD&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My solo-maintainer rules keep EthoPipe within what I can follow and maintain. They include a Python-focused application, a direct-dependency budget, and limits on unnecessary infrastructure. Those are choices for my project, not requirements for yours.&lt;/p&gt;
&lt;p&gt;Here is a &lt;strong&gt;starter template to adapt&lt;/strong&gt;, not a verbatim copy of my rule:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Project: [What I am building]
Stack: [Languages and tools I have chosen]

Before adding a dependency:
Explain why the existing tools are not enough.

Before changing the architecture:
Describe the change and ask whether it belongs to this task.

Leave unrelated files and unfinished ideas alone.
If instructions conflict, ask instead of guessing.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can change these boundaries when I choose another direction. They guide the agent’s actions without turning my current setup into a limit on what I can imagine.&lt;/p&gt;
&lt;h3 id=&quot;3-a-task-procedure-what-am-i-asking-for-now-&quot;&gt;3. A task procedure: what am I asking for now?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;My files:&lt;/strong&gt; &lt;code&gt;idea-to-code.md&lt;/code&gt;, &lt;code&gt;walkthrough-and-tasks.md&lt;/code&gt;, and &lt;code&gt;manage_ideas.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My idea-to-code procedure slows me down between brainstorming and implementation. I want to know why the task matters, what can change, and what result I am looking for. I also ask for a short walkthrough afterward so I can follow what happened.&lt;/p&gt;
&lt;p&gt;This adapted template is enough to start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Objective: [One change I want]
Allowed files: [Existing files involved, including tests]
Leave alone: [Dependencies, configuration, unrelated features]

Before editing:
Read the relevant files and explain the plan.

Check:
[What should happen, and how to try or test it]

If the task needs to grow:
Tell me before making unrelated changes.

When finished:
Summarize changes, checks, blockers, and the next action.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I use numbered idea cards and an idea board when I have more to track. You can begin with a checklist in one file. A checked box is a reminder of work, not a replacement for looking at the result.&lt;/p&gt;
&lt;h3 id=&quot;4-a-checkpoint-where-did-i-stop-&quot;&gt;4. A checkpoint: where did I stop?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;My files:&lt;/strong&gt; &lt;code&gt;project-state.md&lt;/code&gt;, &lt;code&gt;PROJECT_STATE.md&lt;/code&gt;, and &lt;code&gt;manage_project_state.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the part I would most happily recommend to someone who keeps reopening a project and wondering what happened last time. Before finishing, leave a note precise enough for your future self.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Current focus: [The task I am working on]

Completed:
[What changed this session]

Still in progress:
[The file, function, question, or failing check]

Next action:
[One concrete thing to do when I return]

Watch out for:
[Uncommitted changes, blockers, or assumptions]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My script writes an active state file and dated copies, but the format is useful without a script. “Keep working on the API” is less useful to me than “open this test and check this failing case.”&lt;/p&gt;
&lt;h3 id=&quot;5-a-handoff-what-does-a-new-session-need-&quot;&gt;5. A handoff: what does a new session need?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;My files:&lt;/strong&gt; &lt;code&gt;agent-handoff.md&lt;/code&gt;, &lt;code&gt;generate_agent_handoff.py&lt;/code&gt;, and &lt;code&gt;compile_snapshot.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A handoff is a checkpoint with enough context for another conversation. My generator collects Git information and can run tests and refresh a snapshot when requested. The snapshot groups selected files; it is not automatically every file in the repository.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Project context: [What I am building and its current boundaries]
Immediate objective: [The next task]
Relevant files: [Only the context needed for this task]
Recent changes: [What the previous session changed]
Checks: [Observed results, or explicitly not run]
Blockers: [What remains unclear]
Next action: [Where to begin]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fresh conversations help me avoid mixing several tasks. The handoff helps me keep the continuity I actually need.&lt;/p&gt;
&lt;h3 id=&quot;6-a-journal-why-did-i-make-that-choice-&quot;&gt;6. A journal: why did I make that choice?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;My files:&lt;/strong&gt; &lt;code&gt;JOURNEY.md&lt;/code&gt;, &lt;code&gt;manage_journey.py&lt;/code&gt;, and &lt;code&gt;CHANGELOG.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The checkpoint says where to resume. The journal gives me a longer view: what confused me, what I tried, what changed, and what I learned. A changelog has a different job—summarizing project changes for people using or following it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Date and task:
Problem I was trying to solve:
What I tried:
What I changed or decided:
What helped, and what did not:
What I want to try next:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do not need a grand milestone for every entry. A small discovery, an unsuccessful experiment, or a decision not to add something can be worth remembering.&lt;/p&gt;
&lt;h3 id=&quot;7-the-supporting-files-useful-when-you-need-them&quot;&gt;7. The supporting files: useful when you need them&lt;/h3&gt;
&lt;p&gt;You can add automation later. These are the other parts of my collection, grouped by what they help me do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quality checks:&lt;/strong&gt; &lt;code&gt;code-quality-and-standards.md&lt;/code&gt;, &lt;code&gt;audit_maintainer_guardrails.py&lt;/code&gt;, &lt;code&gt;verify_open_source_standards.py&lt;/code&gt;, &lt;code&gt;OPEN_SOURCE_STANDARDS.md&lt;/code&gt;, and &lt;code&gt;OPEN_SOURCE_COMPLIANCE.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release steps:&lt;/strong&gt; &lt;code&gt;release-governance.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounded companion tasks:&lt;/strong&gt; &lt;code&gt;code-companion.md&lt;/code&gt; and &lt;code&gt;jules.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment and session context:&lt;/strong&gt; &lt;code&gt;sync-node.md&lt;/code&gt; and &lt;code&gt;AGENTS.MD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finding the documentation:&lt;/strong&gt; &lt;code&gt;SUMMARY.md&lt;/code&gt; and the checkpoint-archive &lt;code&gt;README.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scripts are tied to my repository and environment. Read them before running them, and adapt paths, commands, and assumptions to your own project. You do not have to introduce a script just to use its underlying idea.&lt;/p&gt;
&lt;h3 id=&quot;8-the-older-files-show-how-the-setup-grew&quot;&gt;8. The older files show how the setup grew&lt;/h3&gt;
&lt;p&gt;My July material includes &lt;code&gt;implementation_plan.md&lt;/code&gt;, &lt;code&gt;walkthrough.md&lt;/code&gt;, &lt;code&gt;walkthrough1.md&lt;/code&gt;, &lt;code&gt;task-project.md&lt;/code&gt;, &lt;code&gt;update.md&lt;/code&gt;, &lt;code&gt;github_actions_audit.md&lt;/code&gt;, &lt;code&gt;github_actions_audit2.md&lt;/code&gt;, &lt;code&gt;technical_audit_report_1.md&lt;/code&gt;, &lt;code&gt;technical_audit_report3.md&lt;/code&gt;, &lt;code&gt;project_technical_report.md&lt;/code&gt;, and &lt;code&gt;agent-handoff-report.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The October records include &lt;code&gt;IDEAS_BOARD.md&lt;/code&gt;, &lt;code&gt;PROJECT_STATE.md&lt;/code&gt;, &lt;code&gt;STATE_20261012_152433.md&lt;/code&gt;, &lt;code&gt;SOLO_MAINTAINER_AUDIT_20261012_151845.md&lt;/code&gt;, &lt;code&gt;SOLO_MAINTAINER_AUDIT_20261012_160520.md&lt;/code&gt;, &lt;code&gt;OPEN_SOURCE_COMPLIANCE.md&lt;/code&gt;, and &lt;code&gt;LLM_SNAPSHOT.MD&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For the bigger narrative, I kept &lt;code&gt;JOURNEY.md&lt;/code&gt;, &lt;code&gt;CHANGELOG.md&lt;/code&gt;, &lt;code&gt;gemini-code-phase.md&lt;/code&gt;, &lt;code&gt;gemini-code-telemetry-sync.md&lt;/code&gt;, &lt;code&gt;project-timeline.md&lt;/code&gt;, and &lt;code&gt;antigravity-system-directive.md&lt;/code&gt;. The earlier &lt;code&gt;Adobe Firefly.docx&lt;/code&gt; article and comment screenshot are the creative starting point, rather than files to install.&lt;/p&gt;
&lt;p&gt;I would browse these for examples rather than copy their historical status into a current project. The July files show individual plans and findings; the October collection shows more repeatable record-keeping. The paperwork grew because I was learning what I needed—not because every project needs that many documents.&lt;/p&gt;
&lt;h3 id=&quot;9-adapt-the-format-to-your-tool&quot;&gt;9. Adapt the format to your tool&lt;/h3&gt;
&lt;p&gt;In Antigravity, &lt;a rel=&quot;nofollow&quot; href=&quot;https://antigravity.google/docs/rules&quot;&gt;rules&lt;/a&gt; are persistent project instructions, while &lt;a rel=&quot;nofollow&quot; href=&quot;https://antigravity.google/docs/skills&quot;&gt;skills&lt;/a&gt; package reusable task procedures. The current documentation explains the supported locations and activation behavior. My older workflow files can supply the content, but their old packaging may need changing.&lt;/p&gt;
&lt;p&gt;If you use another tool, look for its project-instruction and reusable-task features. Ask for help adapting the content, then check the documentation and try a small task. A file is useful only if the tool actually finds it.&lt;/p&gt;
&lt;h3 id=&quot;10-leave-room-for-experiments&quot;&gt;10. Leave room for experiments&lt;/h3&gt;
&lt;p&gt;You might ask for a few approaches without authorizing edits, try one in a separate experiment, and write down what you learned. That can be worthwhile even when nothing gets merged or published.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;These files are meant to make exploration easier—not turn learning into another set of hurdles.&lt;/strong&gt; Keep what helps. Change what does not. Your project can be a place to play.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI assistance:&lt;/strong&gt; I used Gemini and Microsoft Copilot to discuss, organize, and edit this guide. The project files and experience are mine.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/32111/my-ai-coding-project-files-what-you-can-borrow</guid>
<pubDate>Thu, 22 Oct 2026 18:01:20 +0000</pubDate>
</item>
<item>
<title>How Python Makes Coding Easier, Faster, and More Powerful for Developers</title>
<link>https://coderlegion.com/32105/how-python-makes-coding-easier-faster-and-more-powerful-for-developers</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;In today’s technology-driven world, coding is more than just writing lines of code. Developers need to solve problems, automate repetitive tasks, build applications, and create solutions that save time and improve productivity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python has become one of the most useful programming languages because it makes coding easier to learn, faster to develop, and flexible enough for many different projects.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Whether you are a beginner learning programming or an experienced developer building complex applications, Python offers tools and libraries that can help turn ideas into working solutions.&lt;/p&gt;
&lt;h3 id=&quot;why-is-python-so-helpful-for-coding-&quot;&gt;Why Is Python So Helpful for Coding?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Simple and Readable Syntax&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One of Python’s biggest advantages is its readability. Compared with many programming languages, Python allows developers to express ideas with fewer lines of code.&lt;/p&gt;
&lt;p&gt;For example, displaying a message in Python requires only one line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(&quot;Hello, World!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simplicity helps beginners understand programming concepts and allows experienced developers to focus more on solving problems than managing complicated syntax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automation of Repetitive Tasks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Developers often spend time on repetitive activities such as organizing files, processing data, renaming documents, and generating reports.&lt;/p&gt;
&lt;p&gt;Python can automate many of these tasks.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

folder = &quot;documents&quot;

for filename in os.listdir(folder):
    print(&quot;Found file:&quot;, filename)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simple script lists files inside a folder. With additional logic, developers can use Python to organize files, process documents, or automate other routine workflows.&lt;/p&gt;
&lt;p&gt;Automation saves time and reduces the chance of human error.&lt;/p&gt;
&lt;h3 id=&quot;python-for-web-development&quot;&gt;Python for Web Development&lt;/h3&gt;
&lt;p&gt;Python is not limited to small scripts. It is also widely used to build websites, APIs, and backend systems.&lt;/p&gt;
&lt;p&gt;Frameworks such as Django and Flask help developers create web applications more efficiently.&lt;/p&gt;
&lt;p&gt;Python can support features such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User registration and login systems&lt;/li&gt;
&lt;li&gt;Product and inventory management&lt;/li&gt;
&lt;li&gt;Database integration&lt;/li&gt;
&lt;li&gt;Admin dashboards&lt;/li&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;E-commerce backend functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, a developer could use Python and Flask to build a shop management application that allows authorized users to manage products, monitor stock, and track orders.&lt;/p&gt;
&lt;p&gt;Instead of building every feature from scratch, developers can use existing frameworks and libraries to speed up development.&lt;/p&gt;
&lt;h3 id=&quot;python-in-data-analysis-and-artificial-intelligence&quot;&gt;Python in Data Analysis and Artificial Intelligence&lt;/h3&gt;
&lt;p&gt;Modern applications often depend on data. Python provides a strong ecosystem for working with information and building intelligent systems.&lt;/p&gt;
&lt;p&gt;Libraries such as Pandas and NumPy help developers analyze data, while tools such as scikit-learn support machine learning.&lt;/p&gt;
&lt;p&gt;Python is also widely used in artificial intelligence, natural language processing, image recognition, and document processing.&lt;/p&gt;
&lt;p&gt;For example, a developer could create an OCR (Optical Character Recognition) application that extracts text from scanned documents or images.&lt;/p&gt;
&lt;p&gt;This can help businesses convert paper-based information into searchable digital records.&lt;/p&gt;
&lt;h3 id=&quot;how-python-helps-developers-solve-real-problems&quot;&gt;How Python Helps Developers Solve Real Problems&lt;/h3&gt;
&lt;p&gt;Imagine a small business that manages hundreds of products manually. Employees must update stock records, calculate sales, and prepare reports every day.&lt;/p&gt;
&lt;p&gt;This process can be slow and prone to mistakes.&lt;/p&gt;
&lt;p&gt;A Python-based solution could automate calculations, store product information in a database, generate reports, and provide a simple dashboard for employees.&lt;/p&gt;
&lt;p&gt;The result is a more organized workflow and less time spent on repetitive work.&lt;/p&gt;
&lt;p&gt;The key lesson is simple: &lt;strong&gt;coding becomes more valuable when it solves a real problem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Python gives developers a practical way to build these solutions.&lt;/p&gt;
&lt;h3 id=&quot;benefits-of-learning-python-for-developers&quot;&gt;Benefits of Learning Python for Developers&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Faster prototyping:&lt;/strong&gt; Developers can test ideas and build initial versions of applications quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A large library ecosystem:&lt;/strong&gt; Existing packages help developers avoid reinventing common functionality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-platform support:&lt;/strong&gt; Python applications can run on Windows, macOS, and Linux, depending on their dependencies and configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integration with other technologies:&lt;/strong&gt; Python can work with databases, APIs, cloud services, and other programming languages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Career flexibility:&lt;/strong&gt; Python skills are useful in web development, automation, data science, testing, and AI-related projects.&lt;/p&gt;
&lt;h3 id=&quot;is-python-enough-to-become-a-good-developer-&quot;&gt;Is Python Enough to Become a Good Developer?&lt;/h3&gt;
&lt;p&gt;Learning Python is an excellent starting point, but becoming a strong developer requires more than knowing a programming language.&lt;/p&gt;
&lt;p&gt;Developers should also learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Problem-solving and logical thinking&lt;/li&gt;
&lt;li&gt;Data structures and algorithms&lt;/li&gt;
&lt;li&gt;Databases and SQL&lt;/li&gt;
&lt;li&gt;Git and version control&lt;/li&gt;
&lt;li&gt;Testing and debugging&lt;/li&gt;
&lt;li&gt;Application security&lt;/li&gt;
&lt;li&gt;Deployment and maintenance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing code is only one part of software development. Understanding requirements, choosing the right approach, testing carefully, and maintaining applications are equally important.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Python helps developers transform ideas into practical solutions. From automating everyday tasks to building web applications, analyzing data, and developing AI-powered tools, it offers opportunities to create useful software with less complexity.&lt;/p&gt;
&lt;p&gt;The best way to learn Python is to practice by building real projects. Start with a simple automation script, move on to a web application, and gradually explore more advanced technologies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don't just learn Python syntax. Use Python to solve problems, improve workflows, and build something valuable.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At Next Big Creative, we believe that the right technology combined with creative thinking can help businesses grow and work smarter.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nextbigcreative.com/&quot;&gt;nextbigcreative&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32105/how-python-makes-coding-easier-faster-and-more-powerful-for-developers</guid>
<pubDate>Thu, 22 Oct 2026 16:49:13 +0000</pubDate>
</item>
<item>
<title>From Lab to Code #1: Exploring Classical Computer Vision with Python and OpenCV</title>
<link>https://coderlegion.com/32074/from-lab-to-code-1-exploring-classical-computer-vision-with-python-and-opencv</link>
<description>&lt;p&gt;Most conversations about computer vision today revolve around deep learning, neural networks, and increasingly sophisticated AI models.&lt;/p&gt;
&lt;p&gt;But not every image processing problem requires a trained model.&lt;/p&gt;
&lt;p&gt;Sometimes, a Gaussian filter is enough to reduce noise. A threshold can separate objects from their background. A Fourier transform can reveal interference that is difficult to understand in the spatial domain.&lt;/p&gt;
&lt;p&gt;These techniques are relatively simple, mathematically interpretable, and still useful in practical applications.&lt;/p&gt;
&lt;p&gt;Recently, while exploring image and signal processing concepts in my computer engineering studies, I decided to start a small programming series called &lt;strong&gt;From Lab to Code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The idea is simple: take engineering concepts I encounter, build independent implementations, visualize the results, and share what I learn.&lt;/p&gt;
&lt;p&gt;For the first project, I built a small interactive image processing toolkit using Python, OpenCV, NumPy, and Streamlit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 01 — Image Processing:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-are-we-building--1&quot;&gt;What Are We Building?&lt;/h3&gt;
&lt;p&gt;The application explores four classical computer vision techniques:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Image denoising&lt;/strong&gt; using Gaussian and median filters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Object counting&lt;/strong&gt; using illumination correction, thresholding, and connected components.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Periodic noise removal&lt;/strong&gt; using the Fast Fourier Transform (FFT).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Color clustering&lt;/strong&gt; using K-means.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The application provides interactive controls, generated example images, and visual comparisons.&lt;/p&gt;
&lt;p&gt;No pretrained models, API calls, or external datasets are required.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Ertugrulmutlu/from-lab-to-code/main/01-image-processing/results/overview.png&quot; alt=&quot;Image processing examples&quot;&gt;&lt;/p&gt;
&lt;p&gt;All bundled demonstration images are generated programmatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-image-denoising-gaussian-vs-median-filtering&quot;&gt;1. Image Denoising: Gaussian vs. Median Filtering&lt;/h3&gt;
&lt;p&gt;Digital images are often affected by noise.&lt;/p&gt;
&lt;p&gt;Noise can originate from camera sensors, poor lighting conditions, signal transmission, or image acquisition hardware.&lt;/p&gt;
&lt;p&gt;Two common types are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gaussian noise:&lt;/strong&gt; Random variations in pixel intensity, often modeled using a normal distribution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Salt-and-pepper noise:&lt;/strong&gt; Individual pixels unexpectedly taking very dark or very bright values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Different noise distributions require different filtering strategies.&lt;/p&gt;
&lt;h4 id=&quot;gaussian-filtering&quot;&gt;Gaussian Filtering&lt;/h4&gt;
&lt;p&gt;A Gaussian filter performs a weighted average of neighboring pixels.&lt;/p&gt;
&lt;p&gt;Pixels closer to the center receive higher weights, following a Gaussian distribution.&lt;/p&gt;
&lt;p&gt;Mathematically, the two-dimensional Gaussian kernel can be written as:&lt;/p&gt;
&lt;p&gt;G(x, y) = (1 / 2πσ²) × exp(-(x² + y²) / 2σ²)&lt;/p&gt;
&lt;p&gt;The parameter σ controls the amount of smoothing.&lt;/p&gt;
&lt;p&gt;A larger σ produces stronger smoothing but can also remove fine image details.&lt;/p&gt;
&lt;p&gt;In OpenCV, Gaussian filtering is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import cv2

gaussian = cv2.GaussianBlur(
    image, (5, 5), 0
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;median-filtering&quot;&gt;Median Filtering&lt;/h4&gt;
&lt;p&gt;Unlike Gaussian filtering, a median filter replaces the center pixel with the median value of its neighborhood.&lt;/p&gt;
&lt;p&gt;Consider these pixel values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[10, 11, 12, 13, 255]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Their average is 60.2, but their median is 12.&lt;/p&gt;
&lt;p&gt;If the value 255 represents an isolated noise spike, the median is much closer to the surrounding pixel values.&lt;/p&gt;
&lt;p&gt;This makes median filtering particularly useful for salt-and-pepper noise.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;median = cv2.medianBlur(image, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;comparing-the-results&quot;&gt;Comparing the Results&lt;/h4&gt;
&lt;p&gt;The application applies both filters to the same noisy image.&lt;/p&gt;
&lt;p&gt;It also calculates the Mean Squared Error (MSE) against the original image when a reference is available.&lt;/p&gt;
&lt;p&gt;MSE = (1/N) × Σ(original - processed)²&lt;/p&gt;
&lt;p&gt;A lower MSE indicates a smaller average squared difference from the reference image.&lt;/p&gt;
&lt;p&gt;However, MSE alone does not fully describe perceptual image quality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Gaussian filtering is useful for smoothing distributed noise, while median filtering is often more effective against isolated intensity spikes.&lt;/p&gt;
&lt;p&gt;Neither method is universally superior.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-object-counting-from-pixels-to-objects&quot;&gt;2. Object Counting: From Pixels to Objects&lt;/h3&gt;
&lt;p&gt;Suppose we want to count small objects placed on a table.&lt;/p&gt;
&lt;p&gt;At first, this sounds like a simple thresholding problem.&lt;/p&gt;
&lt;p&gt;If the objects are darker than the background, we could classify pixels based on their intensity.&lt;/p&gt;
&lt;p&gt;But what happens when the background illumination is uneven?&lt;/p&gt;
&lt;p&gt;One side of the image might be noticeably darker than the other, making a single global threshold unreliable.&lt;/p&gt;
&lt;h4 id=&quot;step-1-estimate-the-background&quot;&gt;Step 1: Estimate the Background&lt;/h4&gt;
&lt;p&gt;We can approximate slowly changing illumination using a large Gaussian blur.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;background = cv2.GaussianBlur(
    gray, (0, 0), sigmaX=30
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Subtracting the original image from the estimated background helps emphasize darker foreground objects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;corrected = cv2.subtract(
    background, gray
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reduces the influence of gradual illumination changes.&lt;/p&gt;
&lt;h4 id=&quot;step-2-apply-otsu-thresholding&quot;&gt;Step 2: Apply Otsu Thresholding&lt;/h4&gt;
&lt;p&gt;Otsu's method automatically selects a threshold based on the image's intensity distribution.&lt;/p&gt;
&lt;p&gt;Instead of manually choosing a number such as 100 or 150, it searches for a threshold that separates the intensity classes according to their statistical variance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;_, mask = cv2.threshold(
    corrected,
    0,
    255,
    cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is a binary image containing foreground and background pixels.&lt;/p&gt;
&lt;h4 id=&quot;step-3-identify-connected-components&quot;&gt;Step 3: Identify Connected Components&lt;/h4&gt;
&lt;p&gt;After thresholding, neighboring foreground pixels can be grouped into connected regions.&lt;/p&gt;
&lt;p&gt;OpenCV provides a useful function for this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;count, labels, stats, centroids = (
    cv2.connectedComponentsWithStats(
        mask, connectivity=8
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function returns component labels and information such as bounding boxes and pixel areas.&lt;/p&gt;
&lt;p&gt;By excluding the background and removing regions below a minimum area, we can estimate the number of objects.&lt;/p&gt;
&lt;p&gt;The project also applies a small morphological opening operation before counting to remove isolated foreground noise.&lt;/p&gt;
&lt;h4 id=&quot;where-can-this-be-useful-&quot;&gt;Where Can This Be Useful?&lt;/h4&gt;
&lt;p&gt;This approach can be applied to controlled visual inspection tasks involving separate objects, such as small manufactured components.&lt;/p&gt;
&lt;p&gt;However, it comes with important assumptions.&lt;/p&gt;
&lt;p&gt;Objects should be sufficiently separated, and the contrast between objects and background should be reasonably clear.&lt;/p&gt;
&lt;p&gt;Touching objects may merge into a single connected component.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Reliable object counting often requires preprocessing, not just thresholding.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-fourier-transform-removing-periodic-noise&quot;&gt;3. Fourier Transform: Removing Periodic Noise&lt;/h3&gt;
&lt;p&gt;This was one of the most interesting parts of the project.&lt;/p&gt;
&lt;p&gt;Images are normally represented in the spatial domain, where each pixel corresponds to a position.&lt;/p&gt;
&lt;p&gt;But some image artifacts are easier to identify in the frequency domain.&lt;/p&gt;
&lt;p&gt;For example, imagine an image affected by regularly repeating stripes.&lt;/p&gt;
&lt;p&gt;Those stripes represent periodic variations in pixel intensity.&lt;/p&gt;
&lt;h4 id=&quot;moving-to-the-frequency-domain&quot;&gt;Moving to the Frequency Domain&lt;/h4&gt;
&lt;p&gt;The two-dimensional Discrete Fourier Transform decomposes an image into spatial frequency components.&lt;/p&gt;
&lt;p&gt;Each component represents a particular frequency and orientation.&lt;/p&gt;
&lt;p&gt;Using NumPy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np

spectrum = np.fft.fft2(gray)

shifted = np.fft.fftshift(spectrum)

magnitude = np.log1p(
    np.abs(shifted)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fft2()&lt;/code&gt; computes the two-dimensional Fourier transform.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fftshift()&lt;/code&gt; moves the zero-frequency component to the center.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abs()&lt;/code&gt; obtains the magnitude.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log1p()&lt;/code&gt; makes the magnitude spectrum easier to visualize.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Periodic interference may appear as distinct peaks in the Fourier spectrum.&lt;/p&gt;
&lt;h4 id=&quot;removing-specific-frequencies&quot;&gt;Removing Specific Frequencies&lt;/h4&gt;
&lt;p&gt;If we know which frequencies correspond to interference, we can suppress them using a notch filter.&lt;/p&gt;
&lt;p&gt;A notch filter removes selected frequency regions while preserving much of the remaining spectrum.&lt;/p&gt;
&lt;p&gt;For real-valued images, the Fourier spectrum has conjugate symmetry.&lt;/p&gt;
&lt;p&gt;This means a sinusoidal interference component generally produces a symmetric pair of frequency peaks.&lt;/p&gt;
&lt;p&gt;Both should be handled to preserve the symmetry of the filtered spectrum.&lt;/p&gt;
&lt;p&gt;The basic operation is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;filtered_spectrum = shifted * mask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;mask&lt;/code&gt; contains zeros at the frequencies we want to suppress and ones elsewhere.&lt;/p&gt;
&lt;p&gt;We then reconstruct the image using the inverse Fourier transform.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;restored = np.fft.ifft2(
    np.fft.ifftshift(filtered_spectrum)
).real
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;an-important-limitation-1&quot;&gt;An Important Limitation&lt;/h4&gt;
&lt;p&gt;In this project, the periodic noise is generated with known frequencies.&lt;/p&gt;
&lt;p&gt;Therefore, the notch filter already knows where the unwanted frequency components are located.&lt;/p&gt;
&lt;p&gt;This is intentional: it lets us isolate and study the effect of frequency-domain filtering.&lt;/p&gt;
&lt;p&gt;Automatically identifying unknown interference frequencies in arbitrary photographs is a separate problem and is not implemented here.&lt;/p&gt;
&lt;p&gt;A notch filter can also remove useful image information if the rejected frequencies overlap with actual image content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; The Fourier transform provides another perspective on image processing, allowing us to manipulate structures according to their spatial frequencies rather than their pixel positions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-k-means-unsupervised-color-clustering&quot;&gt;4. K-Means: Unsupervised Color Clustering&lt;/h3&gt;
&lt;p&gt;The final technique explores unsupervised learning.&lt;/p&gt;
&lt;p&gt;Unlike classification models, K-means does not require predefined class labels.&lt;/p&gt;
&lt;p&gt;Instead, it groups observations based on similarity.&lt;/p&gt;
&lt;p&gt;For an RGB image, each pixel can be represented as a three-dimensional vector:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Pixel = [Red, Green, Blue]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;K-means attempts to divide these vectors into K clusters by minimizing the squared distances between pixels and their assigned cluster centers.&lt;/p&gt;
&lt;p&gt;The objective function is:&lt;/p&gt;
&lt;p&gt;J = Σ ||xᵢ - μ(clusterᵢ)||²&lt;/p&gt;
&lt;p&gt;where xᵢ is a pixel vector and μ is the corresponding cluster center.&lt;/p&gt;
&lt;h4 id=&quot;implementing-k-means-with-opencv&quot;&gt;Implementing K-Means with OpenCV&lt;/h4&gt;
&lt;p&gt;First, reshape the image into a collection of RGB vectors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pixels = image.reshape(
    -1, 3
).astype(np.float32)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run K-means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;criteria = (
    cv2.TERM_CRITERIA_EPS +
    cv2.TERM_CRITERIA_MAX_ITER,
    25,
    0.2
)

_, labels, centers = cv2.kmeans(
    pixels,
    4,
    None,
    criteria,
    5,
    cv2.KMEANS_PP_CENTERS
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can reconstruct an image by replacing each pixel with the color of its assigned cluster center.&lt;/p&gt;
&lt;p&gt;This produces a simplified image containing a small number of representative colors.&lt;/p&gt;
&lt;h4 id=&quot;color-clustering-is-not-semantic-segmentation&quot;&gt;Color Clustering Is Not Semantic Segmentation&lt;/h4&gt;
&lt;p&gt;This distinction is important.&lt;/p&gt;
&lt;p&gt;K-means only sees RGB values in this implementation.&lt;/p&gt;
&lt;p&gt;It does not understand objects, scenes, or semantic categories.&lt;/p&gt;
&lt;p&gt;Two unrelated objects with similar colors may belong to the same cluster.&lt;/p&gt;
&lt;p&gt;Likewise, one object with multiple colors may be split across several clusters.&lt;/p&gt;
&lt;p&gt;Still, color clustering is useful for exploring image structure, reducing color complexity, and understanding unsupervised learning.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;running-the-project-locally-1&quot;&gt;Running the Project Locally&lt;/h3&gt;
&lt;p&gt;The project is available on GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You will need Python 3.10 or newer.&lt;/p&gt;
&lt;p&gt;Clone the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Ertugrulmutlu/from-lab-to-code.git
cd from-lab-to-code/01-image-processing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Activate it on Windows PowerShell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;.\.venv\Scripts\Activate.ps1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Streamlit will open an interactive application in your browser.&lt;/p&gt;
&lt;p&gt;You can switch between the four techniques, adjust processing parameters, and compare the results.&lt;/p&gt;
&lt;p&gt;Each mode includes a programmatically generated example image. You can also upload an image of your own.&lt;/p&gt;
&lt;p&gt;To regenerate the demonstration gallery:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python demo.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the included tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m unittest discover -s tests -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The processing algorithms are implemented in &lt;code&gt;processing.py&lt;/code&gt;, while &lt;code&gt;synthetic.py&lt;/code&gt; generates the example images and &lt;code&gt;app.py&lt;/code&gt; contains the Streamlit interface.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-learned-1&quot;&gt;What I Learned&lt;/h3&gt;
&lt;p&gt;Building this project reinforced several important ideas.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;understanding the characteristics of a problem matters as much as selecting an algorithm&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Gaussian filtering and median filtering may look similar at first, but they behave differently depending on the noise distribution.&lt;/p&gt;
&lt;p&gt;Second, preprocessing can determine whether a simple computer vision algorithm succeeds or fails.&lt;/p&gt;
&lt;p&gt;In the object counting example, background illumination correction can be just as important as the thresholding operation itself.&lt;/p&gt;
&lt;p&gt;Third, the frequency domain provides a different way to reason about images.&lt;/p&gt;
&lt;p&gt;Some patterns that are difficult to isolate spatially become much easier to analyze after a Fourier transform.&lt;/p&gt;
&lt;p&gt;Finally, unsupervised clustering demonstrates that useful structure can be extracted from images without labeled training data, even though grouping colors is not the same as understanding a scene.&lt;/p&gt;
&lt;h3 id=&quot;why-i-started-from-lab-to-code&quot;&gt;Why I Started From Lab to Code&lt;/h3&gt;
&lt;p&gt;I wanted a lightweight way to turn engineering concepts into practical software projects.&lt;/p&gt;
&lt;p&gt;Not every project needs to become a startup, a large framework, or a long-term product.&lt;/p&gt;
&lt;p&gt;Sometimes, a small, well-explained implementation is enough to explore an idea and demonstrate what you have learned.&lt;/p&gt;
&lt;p&gt;That is the philosophy behind &lt;strong&gt;From Lab to Code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Each entry will focus on an engineering topic, build a small independent implementation, and document the underlying concepts and results.&lt;/p&gt;
&lt;p&gt;The projects are independently implemented, with original demonstration materials rather than redistributed university coursework.&lt;/p&gt;
&lt;p&gt;This first project focuses on classical image processing. Future entries may explore other engineering topics as I encounter them.&lt;/p&gt;
&lt;p&gt;If you are learning computer vision or signal processing, I hope the code and explanations help you experiment with these concepts yourself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the series:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the image processing project:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32074/from-lab-to-code-1-exploring-classical-computer-vision-with-python-and-opencv</guid>
<pubDate>Thu, 22 Oct 2026 12:53:42 +0000</pubDate>
</item>
<item>
<title>We pre-registered a Polymarket insider score. It passed, and we still didn't ship it</title>
<link>https://coderlegion.com/32057/we-pre-registered-a-polymarket-insider-score-it-passed-and-we-still-didnt-ship-it</link>
<description>&lt;p&gt;Polymarket insider trading makes headlines every few weeks, so an insider score sounds like an obvious feature for a Polymarket analytics site. We built one, called it the Early score, wrote the rules down before running it, and tested it on 355,732 wallets. It passed its own test, and we did not release it. The full results are &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-insider-score-entry-price&quot;&gt;on our blog&lt;/a&gt; and the aggregated data and a script are &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/orcalayer/polymarket-insider-score-backtest&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post is about the method: how to set up a wallet score so the test can actually fail, the traps we hit before the test, and the check that changed our mind.&lt;/p&gt;
&lt;h3 id=&quot;1-write-the-rules-down-first&quot;&gt;1. Write the rules down first&lt;/h3&gt;
&lt;p&gt;Before computing a single score, we put the whole method in a pre-registration file: what an entry is, how the move is measured, the shrinkage constant, which markets are out, which periods are compared, and what counts as a pass. Every later change went in as a dated line with the reason.&lt;/p&gt;
&lt;p&gt;The pass rule was simple: score wallets on October 2025 to March 2026 (period A), and the top 10% must still be positive on April to September 2026 (period B), also without the 5 largest events.&lt;/p&gt;
&lt;p&gt;Without this file it is very easy to keep adjusting until something works. With it, every change you make has to be justified in writing before you see the outcome.&lt;/p&gt;
&lt;h3 id=&quot;2-the-score&quot;&gt;2. The score&lt;/h3&gt;
&lt;p&gt;If a wallet knows something first, the price should move its way after it trades. So for each entry we measured the price of the side the wallet holds 24 hours later, minus the price it paid, in cents. A wallet's score is the size-weighted average, pulled toward zero for wallets with few entries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def shrunk_mean(moves, weights, k=5.0):
    &quot;&quot;&quot;Size-weighted mean move, shrunk toward 0.
    weights are entry sizes capped at $10,000, so n_eff counts 'effective entries'.&quot;&quot;&quot;
    total_w = sum(weights)
    if total_w == 0:
        return 0.0
    raw = sum(m * w for m, w in zip(moves, weights)) / total_w
    n_eff = total_w ** 2 / sum(w * w for w in weights)
    return raw * n_eff / (n_eff + k)   # 5 effective entries keep half the raw mean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wallets within 1 cent of zero (one price tick) get a neutral 50; the rest become a percentile. Sports and short Up or Down markets are out: a day after a match the price is the result, so the score would just rank sports bettors.&lt;/p&gt;
&lt;h3 id=&quot;3-trap-wallets-trading-with-each-other-at-made-up-prices&quot;&gt;3. Trap: wallets trading with each other at made-up prices&lt;/h3&gt;
&lt;p&gt;On one test day the top of our list was a group of related wallets passing the same shares back and forth in finished tennis matches. One bought at 0.02 and sold to a partner at 0.96 four minutes later. Read trade by trade, that is a +94 cent entry.&lt;/p&gt;
&lt;p&gt;The fix was to define an entry as a wallet's &lt;strong&gt;net position change in one market over one UTC day&lt;/strong&gt;, not as a single trade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from collections import defaultdict

def daily_entries(trades):
    &quot;&quot;&quot;Simplified. trades: dicts with wallet, market, day, side ('YES'/'NO'), shares (+buy/-sell), price.
    Selling NO counts as buying YES. A same-day round trip nets to zero.&quot;&quot;&quot;
    net = defaultdict(lambda: {&quot;yes&quot;: 0.0, &quot;no&quot;: 0.0, &quot;cost_yes&quot;: 0.0, &quot;cost_no&quot;: 0.0})
    for t in trades:
        k = (t[&quot;wallet&quot;], t[&quot;market&quot;], t[&quot;day&quot;])
        s = &quot;yes&quot; if t[&quot;side&quot;] == &quot;YES&quot; else &quot;no&quot;
        net[k][s] += t[&quot;shares&quot;]
        net[k][&quot;cost_&quot; + s] += t[&quot;shares&quot;] * t[&quot;price&quot;]
    for k, v in net.items():
        delta = v[&quot;yes&quot;] - v[&quot;no&quot;]
        if delta == 0:
            continue
        side = &quot;yes&quot; if abs(v[&quot;yes&quot;]) &amp;gt;= abs(v[&quot;no&quot;]) else &quot;no&quot;
        avg_price = v[&quot;cost_&quot; + side] / v[side] if v[side] else None
        yield k, delta, avg_price
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you rank Polymarket wallets by anything trade-based, expect these pairs near the top of your list.&lt;/p&gt;
&lt;h3 id=&quot;4-trap-a-thin-price-after-&quot;&gt;4. Trap: a thin &quot;price after&quot;&lt;/h3&gt;
&lt;p&gt;In one market a handful of trades of $1.50 to $15 at 0.15 set the price 24 hours later, though the market settled at 1. One tiny trade can fake a move. We now require at least $100 of &lt;strong&gt;other wallets'&lt;/strong&gt; trades in that hour before using a price, and fall back to the final price only if the market has closed.&lt;/p&gt;
&lt;h3 id=&quot;5-the-test-passed&quot;&gt;5. The test passed&lt;/h3&gt;
&lt;p&gt;Period A groups, measured on period B, with t-statistics clustered by event (many entries in one event are not independent):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Group from period A &lt;/th&gt;&lt;th&gt;24 h move in B, cents per entry &lt;/th&gt;&lt;th&gt;t&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Top 10% &lt;/td&gt;&lt;td&gt;+0.83 &lt;/td&gt;&lt;td&gt;7.9&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Bottom 10% &lt;/td&gt;&lt;td&gt;-0.76 &lt;/td&gt;&lt;td&gt;-4.7&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;All wallets &lt;/td&gt;&lt;td&gt;-0.01 &lt;/td&gt;&lt;td&gt;-0.2&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It held without the 5 largest events, without weather markets, and for every shrinkage constant from 0 to 100. By the rule we wrote, a pass.&lt;/p&gt;
&lt;h3 id=&quot;6-the-check-that-changed-our-mind-look-at-the-mechanism&quot;&gt;6. The check that changed our mind: look at the mechanism&lt;/h3&gt;
&lt;p&gt;A pass tells you the score finds &lt;em&gt;something&lt;/em&gt; that repeats. It does not tell you what. An insider and a wallet that simply buys cheaper both show a positive move, but with a different shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;news spreading: the price keeps moving for hours;&lt;/li&gt;
&lt;li&gt;a cheap fill: the gap is there at once and stops growing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we split each wallet's move into the first hour and the drift from 1 to 24 hours, and asked which part repeats between the two periods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from statistics import correlation  # Python 3.10+

def ranks(xs):
    order = sorted(range(len(xs)), key=xs.__getitem__)
    r = [0] * len(xs)
    for i, j in enumerate(order):
        r[j] = i
    return r

def spearman(a, b):
    return correlation(ranks(a), ranks(b))

# per wallet, period A vs period B
rho_first_hour = spearman(first_hour_A, first_hour_B)   # 0.138
rho_drift      = spearman(drift_A, drift_B)             # 0.050
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The top 10% were already +0.75 cents ahead after one hour and +0.83 after 24. The first-hour part repeats; the drift, the part an insider score exists to catch, barely does. The score was ranking wallets by fill quality.&lt;/p&gt;
&lt;p&gt;This check was not in the pre-registration, and we label it exploratory everywhere. But it is the one that decided whether a label like &quot;insider&quot; would tell users the truth. It would not, so the score is not on the site, in the API or in MCP.&lt;/p&gt;
&lt;h3 id=&quot;7-money-not-moves&quot;&gt;7. Money, not moves&lt;/h3&gt;
&lt;p&gt;A 24-hour move is not profit, so we also held every entry in a resolved market to settlement and subtracted the fee each wallet actually paid on-chain. The top 10% made +1.17 cents per entry after fees (t 2.9), on both mostly taker and mostly maker entries. One detail that matters if you repeat this: compare groups with &lt;strong&gt;all wallets&lt;/strong&gt;, not with zero. On longer-dated markets every group drifts up, so zero is the wrong baseline.&lt;/p&gt;
&lt;h3 id=&quot;reproduce-it&quot;&gt;Reproduce it&lt;/h3&gt;
&lt;p&gt;The repo has the aggregated tables behind every number and &lt;code&gt;scripts/early_score_check.py&lt;/code&gt; (standard library only), which rebuilds entries, scores and the period test from the OrcaLayer API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export ORCALAYER_API_KEY=sk_orca_...        # never commit it
python3 scripts/early_score_check.py --csv entries.csv        # first 1,000 markets of data/markets.csv
python3 scripts/early_score_check.py --load-csv entries.csv   # rerun the analysis, no API calls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The API's trade history starts on May 1, 2026, so the script scores on May and June and checks on July to September, on a fixed list of 1,000 markets. The script writes wallets only as salted hashes, and the repo publishes groups only, no addresses.&lt;/p&gt;
&lt;h3 id=&quot;what-this-does-not-show&quot;&gt;What this does not show&lt;/h3&gt;
&lt;p&gt;Sports, individual wallets, and copying these wallets (a copy trades after them and does not get their price). On the shorter API check, the groups did not separate when held to settlement, so treat the money part as the weakest result.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-insider-score-entry-price&quot;&gt;orcalayer.com&lt;/a&gt;. OrcaLayer is an independent Polymarket analytics provider, not affiliated with Polymarket.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32057/we-pre-registered-a-polymarket-insider-score-it-passed-and-we-still-didnt-ship-it</guid>
<pubDate>Thu, 22 Oct 2026 09:55:28 +0000</pubDate>
</item>
<item>
<title>DuckDB 2.0 vs Polars vs Spark on One Machine: 150M Rows Under a 4.5 GiB Cap</title>
<link>https://coderlegion.com/32051/duckdb-2-0-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-4-5-gib-cap</link>
<description>&lt;p&gt;Originally published on DEV.to: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/duckdb-20-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-45-gib-cap-h38&quot;&gt;https://dev.to/firfircelik/duckdb-20-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-45-gib-cap-h38&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;DuckDB 2.0.0 shipped on October 21, 2026 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/2026/10/21/announcing-duckdb-200&quot;&gt;announcement&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/duckdb/2.0.0/&quot;&gt;PyPI&lt;/a&gt;). The next morning I ran the same single-machine benchmark I had already run on DuckDB 1.5.6: three engines, one 8 vCPU VM, Parquet in and Parquet out, a hard memory cap, and four queries that look like real data engineering work.&lt;/p&gt;
&lt;p&gt;The thesis I started with, and that the numbers support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DuckDB beats Spark-on-one-box on memory-bound DE work.&lt;/strong&gt; It finished every run, at every size, 5 to 12 times faster than local PySpark at 150M rows, and it still finished the heavy queries with a 1 GB memory limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polars hits a wall on window dedup.&lt;/strong&gt; At 150M rows the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; style &quot;latest row per customer&quot; query never finished under the cap. Not at 4.5 GiB, not with an out-of-core budget, not at 9 GiB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polars is not slow.&lt;/strong&gt; On filters, joins and a high-cardinality group by it ties DuckDB or beats it, when it fits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything below is measured on that one box. Where I interpret or give an opinion, I say so.&lt;/p&gt;
&lt;h3 id=&quot;the-contenders&quot;&gt;The contenders&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Model &lt;/td&gt;&lt;td&gt;In-process SQL engine &lt;/td&gt;&lt;td&gt;In-process DataFrame engine, lazy + streaming &lt;/td&gt;&lt;td&gt;JVM engine, &lt;code&gt;local[8]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;How I drove it &lt;/td&gt;&lt;td&gt;&lt;code&gt;COPY (SQL) TO 'out.parquet'&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;LazyFrame.sink_parquet()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;spark.sql(...).write.parquet()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memory knob &lt;/td&gt;&lt;td&gt;&lt;code&gt;SET memory_limit = '3GB'&lt;/code&gt; &lt;/td&gt;&lt;td&gt;default (plus one run with an out-of-core budget) &lt;/td&gt;&lt;td&gt;&lt;code&gt;spark.driver.memory = 3g&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Threads &lt;/td&gt;&lt;td&gt;8 &lt;/td&gt;&lt;td&gt;8 (&lt;code&gt;POLARS_MAX_THREADS&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Polars 2.0.0 was published on PyPI on &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/polars/2.0.0/&quot;&gt;October 6, 2026&lt;/a&gt;, PySpark 4.2.0 on &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/pyspark/4.2.0/&quot;&gt;July 14, 2026&lt;/a&gt;. DuckDB 2.0.0 wheels landed on PyPI on October 21, the same day as the &lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/release_calendar&quot;&gt;release calendar&lt;/a&gt; entry.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hardware and versions&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VM: 8 vCPU Intel Xeon, 15 GiB RAM, no swap, Linux 6.12. It is a shared box, so other processes were running. Every run waited until at least 5 GiB was free and CPU busy was under 30% over a 3 second window.&lt;/li&gt;
&lt;li&gt;Python 3.13.5, OpenJDK 21.0.12, pyarrow 26.0.0.&lt;/li&gt;
&lt;li&gt;DuckDB 2.0.0, Polars 2.0.0, PySpark 4.2.0. The &quot;before&quot; numbers are DuckDB 1.5.6 on the same box and data, run on October 19.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Data.&lt;/strong&gt; A seeded synthetic star schema written with pyarrow: a &lt;code&gt;sales&lt;/code&gt; fact table and a 50,000-row &lt;code&gt;products&lt;/code&gt; dimension. Three sizes: 10M rows (213 MB), 50M (1.1 GB) and 150M (3.2 GB of snappy Parquet, 30 files of 5M rows). About 10 orders per customer, so 150M rows means roughly 15M customers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Queries.&lt;/strong&gt; The same SQL for DuckDB and Spark, the same logic as Polars expressions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;q1_filter_agg&lt;/code&gt;: filter on a timestamp and status, group by store and status.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q2_join_agg&lt;/code&gt;: fact-to-dimension join, group by category and brand.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q3_window_latest&lt;/code&gt;: latest order per customer with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;. The classic dedup and SCD pattern.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q4_highcard_agg&lt;/code&gt;: one row per customer (about 15M groups at 150M).&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- q3_window_latest
SELECT customer_id, order_id, ts, qty * price AS amount
FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id
                                 ORDER BY ts DESC, order_id DESC) AS rn
    FROM sales
) t
WHERE rn = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Method.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every run is a fresh process: interpreter start, import, session, read Parquet, compute, write Parquet. So no warm caches inside the engine, and Spark pays its JVM start every time.&lt;/li&gt;
&lt;li&gt;Engines run strictly one at a time. The engine order rotates per repetition so no engine always goes first.&lt;/li&gt;
&lt;li&gt;5 repetitions at 10M, 3 at 50M and 150M. I report &lt;strong&gt;medians&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A watchdog samples the RSS of the whole process tree every 50 ms and kills the run if it goes above &lt;strong&gt;4.5 GiB&lt;/strong&gt;. That is the cap. It is deliberately tighter than the box, because &quot;fits in a small container&quot; is the question I care about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wall&lt;/strong&gt; is the whole process. &lt;strong&gt;Query&lt;/strong&gt; is read, compute and write only. The difference is setup.&lt;/li&gt;
&lt;li&gt;Correctness: a validation script compares row counts and rounded checksums of every output. Where an engine produced output, all three engines agreed, for every query and size.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;results-at-150m-rows&quot;&gt;Results at 150M rows&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Query &lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Filter + aggregate &lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.87 s&lt;/strong&gt; (3/3) &lt;/td&gt;&lt;td&gt;0.96 s (3/3) &lt;/td&gt;&lt;td&gt;10.77 s (3/3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Join + aggregate &lt;/td&gt;&lt;td&gt;1.47 s (3/3) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;1.46 s&lt;/strong&gt; (3/3) &lt;/td&gt;&lt;td&gt;16.10 s (3/3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;&lt;strong&gt;7.92 s&lt;/strong&gt; (3/3) &lt;/td&gt;&lt;td&gt;killed (0/3) &lt;/td&gt;&lt;td&gt;46.29 s (2/3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;High-cardinality group by &lt;/td&gt;&lt;td&gt;7.91 s (3/3) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;4.21 s&lt;/strong&gt; (1/3) &lt;/td&gt;&lt;td&gt;39.70 s (3/3)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Median wall seconds, runs that finished under the 4.5 GiB cap in brackets.&lt;/p&gt;
&lt;p&gt;Peak RSS (median of finished runs) tells the other half:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Query &lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Filter + aggregate &lt;/td&gt;&lt;td&gt;0.77 GiB &lt;/td&gt;&lt;td&gt;0.94 GiB &lt;/td&gt;&lt;td&gt;1.85 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Join + aggregate &lt;/td&gt;&lt;td&gt;0.42 GiB &lt;/td&gt;&lt;td&gt;1.96 GiB &lt;/td&gt;&lt;td&gt;1.61 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;3.01 GiB &lt;/td&gt;&lt;td&gt;above 4.5 (killed) &lt;/td&gt;&lt;td&gt;3.74 GiB, one run hit 7.51&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;High-cardinality group by &lt;/td&gt;&lt;td&gt;3.01 GiB &lt;/td&gt;&lt;td&gt;4.49 GiB &lt;/td&gt;&lt;td&gt;4.40 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three things stand out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark on one box pays twice.&lt;/strong&gt; About 5 seconds of every Spark run is setup (median 5.0 to 5.8 s, JVM plus session) before it reads a byte. Then the query itself is slower: query-only time was 7.4x DuckDB's on the filter, 7.9x on the join, 5.2x on the window dedup and 4.3x on the group by. In wall time that is 5x to 12x. And &lt;code&gt;spark.driver.memory = 3g&lt;/code&gt; did not keep the process at 3 GiB: one window dedup run reached 7.51 GiB of process-tree RSS and was killed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Polars is fast right up to the cap.&lt;/strong&gt; It tied DuckDB on filter and join and was the fastest engine on the high-cardinality group by (4.21 s against 7.91 s). But only 1 of 3 runs finished: that one peaked at 4.49 GiB, and the other two crossed the cap at 4.50 and 4.55 GiB. Fast and close to the edge is a fine place for a notebook and a bad place for a scheduled job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The window dedup is where Polars stops.&lt;/strong&gt; At 150M rows it was killed in all 3 runs, within a few seconds of starting, at 4.52 to 4.66 GiB.&lt;/p&gt;
&lt;h3 id=&quot;polars-and-the-window-wall&quot;&gt;Polars and the window wall&lt;/h3&gt;
&lt;p&gt;I gave Polars two more chances on the 150M window dedup:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Polars 2.0.0, 150M window dedup &lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Default settings, 4.5 GiB cap (Oct 22) &lt;/td&gt;&lt;td&gt;0/3, killed at 4.52 to 4.66 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POLARS_OOC_MEMORY_BUDGET_MB=3072&lt;/code&gt;, 4.5 GiB cap (Oct 22) &lt;/td&gt;&lt;td&gt;0/3, killed at 4.57 to 4.66 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Default settings, &lt;strong&gt;9 GiB&lt;/strong&gt; cap (Oct 19, same Polars 2.0.0) &lt;/td&gt;&lt;td&gt;0/3, killed at 9.04 to 9.54 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The out-of-core budget variable is one I found in the Polars 2.0.0 source, not a documented knob, so treat that row as &quot;I tried&quot;, not as guidance. The same variable did help the group by: with it, Polars finished 3/3 at 4.67 s and 4.18 to 4.30 GiB.&lt;/p&gt;
&lt;p&gt;At smaller sizes the dedup works, but the shape is already visible:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Window dedup &lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;10M rows &lt;/td&gt;&lt;td&gt;0.60 s, 0.65 GiB &lt;/td&gt;&lt;td&gt;1.83 s, 1.20 GiB &lt;/td&gt;&lt;td&gt;12.74 s, 2.94 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;50M rows &lt;/td&gt;&lt;td&gt;2.33 s, 2.19 GiB &lt;/td&gt;&lt;td&gt;10.36 s, 4.46 GiB &lt;/td&gt;&lt;td&gt;29.06 s, 3.77 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;150M rows &lt;/td&gt;&lt;td&gt;7.92 s, 3.01 GiB &lt;/td&gt;&lt;td&gt;killed &lt;/td&gt;&lt;td&gt;46.29 s, 3.74 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At 50M Polars already used 4.46 GiB for a query DuckDB did in 2.19 GiB, and took 4.4x longer.&lt;/p&gt;
&lt;p&gt;This is the Polars expression I used. It is the direct translation of the SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import polars as pl

amount = pl.col(&quot;qty&quot;).cast(pl.Float64) * pl.col(&quot;price&quot;)
rn = pl.int_range(pl.len()).over(
    &quot;customer_id&quot;, order_by=[&quot;ts&quot;, &quot;order_id&quot;], descending=True
)
(
    pl.scan_parquet(&quot;data/sales_150m/*.parquet&quot;)
    .filter(rn == 0)
    .select(&quot;customer_id&quot;, &quot;order_id&quot;, &quot;ts&quot;, amount.alias(&quot;amount&quot;))
    .sink_parquet(&quot;out/polars/150m_q3_window_latest.parquet&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fair caveat: I did not test other Polars formulations of the same dedup (a sort followed by &lt;code&gt;unique(keep=&quot;first&quot;)&lt;/code&gt;, or a &lt;code&gt;group_by().agg(...)&lt;/code&gt; with &lt;code&gt;sort_by().last()&lt;/code&gt;). One of them may stream better. The point stands for the common case, though: the first thing a team writes is the window, and that one did not fit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (Oct 22):&lt;/strong&gt; after this went out, Victor Cid (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vleyked&quot;&gt;@Vleyked&lt;/a&gt;) tested exactly that in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/1&quot;&gt;engine-bench PR #1&lt;/a&gt;. His &lt;code&gt;sort&lt;/code&gt; + &lt;code&gt;unique(keep=&quot;first&quot;)&lt;/code&gt; version drops the in-memory window fallback from the plan and finished 3/3 at 150M rows under the same 4.5 GiB cap, peaking at 2.50 to 2.74 GiB, but only with &lt;code&gt;POLARS_OOC_MEMORY_BUDGET_MB=1024&lt;/code&gt;. With default settings or a 3072 MB budget it still went over the cap. The price is speed: a 90.58 s median against 17.56 s for DuckDB on the same 4 vCPU runner, so roughly 5x slower. His output matched DuckDB row for row (14,999,347 rows). So Polars can do this dedup under the cap if you rewrite it and tune the spill budget, and with a smarter query it gets close to DuckDB (see below). His runs were on different hardware, so they are not in the tables above.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second update:&lt;/strong&gt; in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/2&quot;&gt;engine-bench PR #2&lt;/a&gt; he filtered each customer down to their max timestamp first and only sorted those rows to break ties. That brought the median down to 14.23 s at 150M rows with default Polars settings, peaking at 4.04 GiB under the same cap, with output matching DuckDB row for row. On his runner DuckDB's reference was a single 18.50 s run, so I'm not calling that a repeated cross-engine win. The real lesson is that the first way you write a Polars dedup can decide whether it finishes at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third update (Oct 23):&lt;/strong&gt; Ritchie Vink, the creator of Polars, pointed out that only DuckDB was told about the memory limit, so Polars did not know it had to stay under 4 GiB, and suggested setting POLARS_OOC_MEMORY_BUDGET_MB for a fairer comparison. I reran every Polars query at 150M rows on the same box under the same 4.5 GiB cap with that budget, next to a DuckDB rerun in the same session: the high-cardinality group by finished 3/3 at 6.17 s and 3.46 GiB with a 3072 MB budget, against 9.56 s for DuckDB (the box was busier than on Oct 22 and DuckDB ran slower too, so compare within this session, not with the tables above; the Oct 22 run with the same budget gave 4.67 s). The window dedup with over(order_by) still went over the cap in every run at 3072, 1024 and 512 MB, Victor's sort plus unique version fit only at 1024 MB (31.28 s, 2.29 GiB), and the max-timestamp rewrite finished 3/3 on default settings (5.61 s, 4.02 GiB). Ritchie says Polars main now spills the group by correctly and that this ships in the next release, which was not on PyPI yet when I ran this, so I will rerun when it ships. Raw results and output checks are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/3&quot;&gt;engine-bench PR #3&lt;/a&gt; and in results/polars_ooc_budget_2026-10-23 on the polars-ooc-budget-2026-10-23 branch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fourth update (Oct 26):&lt;/strong&gt; after the third update I reported the window dedup to Polars as &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/pola-rs/polars/issues/30270&quot;&gt;pola-rs/polars#30270&lt;/a&gt;: the over(order_by) window inside filter() ignored POLARS_OOC_MEMORY_BUDGET_MB and ran in an in-memory fallback node. Ritchie Vink fixed it (#30272, #30282 and #30280), and the fixes shipped in Polars 2.1.0. I reran the 150M benchmark on the official 2.1.0 wheel on the same box, with the same harness and the same 4.5 GiB cap, next to a DuckDB rerun in the same session. With a budget set, the window dedup now finishes 3/3 at every budget I tried: 18.78 s and 3.34 GiB at 3072 MB, 20.11 s and 1.29 GiB at 1024 MB, 43.01 s and 2.15 GiB at 512 MB. On 2.0.0 it failed at all three. Memory now follows the budget closely: at 512 MB, Victor's sort plus unique version peaked at 0.69 GiB (73.85 s) and the high-cardinality group by at 0.74 GiB (9.43 s). DuckDB still ran the window dedup faster in the same session, 8.07 s, about 2.3x quicker than Polars at 3072 MB, and the max-timestamp rewrite remains the fastest Polars dedup (5.12 s on default settings). Every finished output matched DuckDB row for row (14,999,347 rows). Two caveats. Without a budget, the window dedup and sort plus unique still went over the cap. My cap is a process watchdog, not a cgroup limit, so the new cgroup-aware default in 2.1.0 had nothing to detect and is untested here; set the budget explicitly. One sort plus unique run at 3072 MB took 285.64 s for no clear reason, so I report medians of 3 runs. Raw results, plans and output checks: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/4&quot;&gt;engine-bench PR #4&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/blob/polars-main-window-fix-2026-10-24/results/polars_2.1.0_2026-10-25/README.md&quot;&gt;2.1.0 results README&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;duckdb-under-pressure-memory-limit-1-gb&quot;&gt;DuckDB under pressure: memory_limit = 1 GB&lt;/h3&gt;
&lt;p&gt;The interesting DuckDB question is not &quot;how fast with room to spare&quot; but &quot;what happens when the box is small&quot;. Same 150M rows, &lt;code&gt;memory_limit&lt;/code&gt; cut to 1 GB:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;150M, &lt;code&gt;memory_limit = 1GB&lt;/code&gt; &lt;/th&gt;&lt;th&gt;Wall (median) &lt;/th&gt;&lt;th&gt;Peak RSS &lt;/th&gt;&lt;th&gt;Finished&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;14.97 s &lt;/td&gt;&lt;td&gt;1.12 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;High-cardinality group by &lt;/td&gt;&lt;td&gt;13.94 s &lt;/td&gt;&lt;td&gt;1.08 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Window dedup, &lt;code&gt;preserve_insertion_order = false&lt;/code&gt; &lt;/td&gt;&lt;td&gt;14.94 s &lt;/td&gt;&lt;td&gt;1.12 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Group by, &lt;code&gt;preserve_insertion_order = false&lt;/code&gt; &lt;/td&gt;&lt;td&gt;13.88 s &lt;/td&gt;&lt;td&gt;1.08 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With a third of the memory, DuckDB spilled to disk and still beat Spark's unconstrained run by about 3x on both queries, at a quarter to a third of Spark's peak RSS. The DuckDB docs list window functions with &lt;code&gt;PARTITION BY ... ORDER BY&lt;/code&gt; among the blocking operators it can process larger than memory (&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads&quot;&gt;How to Tune Workloads&lt;/a&gt;). This run is that sentence, measured.&lt;/p&gt;
&lt;p&gt;The setup is three lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import duckdb

con = duckdb.connect()
con.execute(&quot;SET memory_limit = '1GB'&quot;)
con.execute(&quot;SET threads = 8&quot;)
con.execute(&quot;SET temp_directory = '/mnt/fast-local-disk/duckdb_tmp'&quot;)  # where spills go
con.execute(&quot;&quot;&quot;
    COPY (
        SELECT customer_id, order_id, ts, qty * price AS amount
        FROM (
            SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id
                                         ORDER BY ts DESC, order_id DESC) AS rn
            FROM read_parquet('data/sales_150m/*.parquet')
        )
        WHERE rn = 1
    ) TO 'out/latest_per_customer.parquet' (FORMAT parquet)
&quot;&quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;duckdb-1-5-6-vs-2-0-0&quot;&gt;DuckDB 1.5.6 vs 2.0.0&lt;/h3&gt;
&lt;p&gt;Same box, same data, same scripts. 1.5.6 ran on October 19, 2.0.0 on October 22. Median wall seconds at 150M:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Setting &lt;/th&gt;&lt;th&gt;Query &lt;/th&gt;&lt;th&gt;1.5.6 &lt;/th&gt;&lt;th&gt;2.0.0 &lt;/th&gt;&lt;th&gt;Change&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 3GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;9.09 &lt;/td&gt;&lt;td&gt;7.92 &lt;/td&gt;&lt;td&gt;-13%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 3GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Group by &lt;/td&gt;&lt;td&gt;9.61 &lt;/td&gt;&lt;td&gt;7.91 &lt;/td&gt;&lt;td&gt;-18%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 1GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;17.79 &lt;/td&gt;&lt;td&gt;14.97 &lt;/td&gt;&lt;td&gt;-16%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 1GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Group by &lt;/td&gt;&lt;td&gt;22.06 &lt;/td&gt;&lt;td&gt;13.94 &lt;/td&gt;&lt;td&gt;-37%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1GB, no insertion order &lt;/td&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;14.85 &lt;/td&gt;&lt;td&gt;14.94 &lt;/td&gt;&lt;td&gt;+1%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1GB, no insertion order &lt;/td&gt;&lt;td&gt;Group by &lt;/td&gt;&lt;td&gt;15.15 &lt;/td&gt;&lt;td&gt;13.88 &lt;/td&gt;&lt;td&gt;-8%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The useful finding is the last four rows together. On 1.5.6, &lt;code&gt;SET preserve_insertion_order = false&lt;/code&gt; was worth 16% on the dedup and 31% on the group by under a 1 GB limit. On 2.0.0 the default already lands where the tuned setting did, and the flag changes almost nothing for these two queries. If you carry that setting in job configs as a performance trick, re-measure after upgrading. It still matters for what it was designed for: the docs recommend it when large imports or exports run out of memory, and it lets DuckDB reorder rows without an &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Not everything improved. The 50M window dedup was slower on 2.0.0 (1.94 s to 2.33 s), and peak RSS for the light queries went up (150M filter: 0.26 to 0.77 GiB, join: 0.23 to 0.42 GiB), while the heavy queries went down (window dedup: 3.47 to 3.01 GiB). I have not investigated why. The two runs were also three days apart on a shared box, so treat differences under about 10 to 15% as noise. The 1 GB group by drop is far outside that.&lt;/p&gt;
&lt;p&gt;The release itself is mostly a feature release: Quack and &lt;code&gt;CONNECT&lt;/code&gt; for client-server use, asynchronous I/O, a first-class &lt;code&gt;VARIANT&lt;/code&gt;, triggers, a PEG parser by default (&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/2026/10/21/announcing-duckdb-200&quot;&gt;announcement&lt;/a&gt;). It is an LTS line supported until at least October 2027. One migration note matters for pipelines: database files written by 2.0 cannot be read by versions before 2.0. My benchmark only reads and writes Parquet, so it never touched that.&lt;/p&gt;
&lt;h3 id=&quot;where-spark-still-wins&quot;&gt;Where Spark still wins&lt;/h3&gt;
&lt;p&gt;None of this says &quot;replace Spark&quot;. It says Spark on one box is the wrong comparison for small and medium jobs. Spark still wins when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The data does not fit one node's disk and memory, or the deadline does not fit one node's cores.&lt;/strong&gt; This test stops at 3.2 GB of Parquet on purpose. DuckDB spills well, but one machine is still one machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The platform is built around it.&lt;/strong&gt; On Databricks and Fabric, Spark is where Unity Catalog or OneLake governance, Delta writes with concurrent writers, lineage, job orchestration and streaming already live. A DuckDB step outside that boundary is a step you have to secure, monitor and explain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The cluster is already running.&lt;/strong&gt; The 5 second JVM start I measured per run disappears in a warm interactive cluster or a long job. The query-only gap (4x to 8x here) does not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The team is a Spark team.&lt;/strong&gt; One engine everyone can debug at 02:00 is worth more than a faster engine one person understands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My opinion as a Lead DE on Azure, Databricks and Fabric: the line is not the engine, it is the size of the job. Most jobs I see are tens of millions of rows dressed up as big data and started on a cluster. For those (file-to-file transforms before landing, validation and reconciliation checks, small dimension builds, CI tests for SQL logic) a single-node engine with a memory limit is the cheaper and more predictable choice. Anything that writes governed Delta tables stays on the platform.&lt;/p&gt;
&lt;h3 id=&quot;failure-modes-i-hit-so-you-don-t-have-to&quot;&gt;Failure modes I hit, so you don't have to&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Engine memory settings are not process memory.&lt;/strong&gt; Spark's 3g driver heap still produced a 7.51 GiB process. DuckDB's whole process stayed close to its &lt;code&gt;memory_limit&lt;/code&gt;: about 3.0 GiB peak at a 3 GB limit, 1.1 GiB at 1 GB. Size containers on measured RSS, not on the setting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Streaming&quot; does not mean bounded.&lt;/strong&gt; Polars streamed the filter, join and group by fine. The window dedup with an &lt;code&gt;over(order_by=...)&lt;/code&gt; did not stay bounded, even with 9 GiB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Close to the cap is a failure that hasn't happened yet.&lt;/strong&gt; Polars' group by finished in 1 of 3 runs, peaking at 4.49 GiB against a 4.5 GiB cap. Same query, same data, same version. Schedule that and it fails some nights.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared hosts lie.&lt;/strong&gt; One Spark run at 10M was killed because free memory on the whole box fell under my 1 GiB safety floor while Spark itself was at 1.6 GiB. That was the neighbours, not Spark. Gate each run on free memory and CPU, and record both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate outputs, not just timings.&lt;/strong&gt; Every engine produced the same row counts and checksums where it finished. Without that check, a fast wrong answer looks like a win.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;reproduce-it-1&quot;&gt;Reproduce it&lt;/h3&gt;
&lt;p&gt;Everything is public at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench&quot;&gt;firfircelik/engine-bench&lt;/a&gt;: scripts, raw results, pinned versions and the query plans. Here are the parts that matter. The data generator is seeded (&lt;code&gt;SEED = 20261019&lt;/code&gt;) and writes 5M-row chunks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;rng = np.random.default_rng([SEED, chunk_index])
t = pa.table({
    &quot;order_id&quot;: np.arange(lo, lo + n, dtype=np.int64),
    &quot;ts&quot;: pa.array(start + rng.integers(0, 270 * 24 * 3600, n), pa.timestamp(&quot;s&quot;)),
    &quot;customer_id&quot;: rng.integers(0, rows // 10, n, dtype=np.int32),
    &quot;product_id&quot;: rng.integers(0, 50_000, n, dtype=np.int32),
    &quot;store_id&quot;: rng.integers(0, 500, n, dtype=np.int16),
    &quot;qty&quot;: rng.integers(1, 10, n, dtype=np.int16),
    &quot;price&quot;: np.round(rng.uniform(1.0, 500.0, n), 2),
    &quot;status&quot;: rng.choice(STATUSES, n, p=[0.55, 0.20, 0.12, 0.08, 0.05]),
})
pq.write_table(t, f&quot;sales_{rows // 1_000_000}m/part-{chunk_index:04d}.parquet&quot;,
               row_group_size=1_000_000, compression=&quot;snappy&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spark session, the only non-default settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;spark = (SparkSession.builder.master(&quot;local[8]&quot;)
         .config(&quot;spark.driver.memory&quot;, &quot;3g&quot;)
         .config(&quot;spark.local.dir&quot;, TMP)
         .config(&quot;spark.ui.enabled&quot;, &quot;false&quot;)
         .config(&quot;spark.sql.session.timeZone&quot;, &quot;UTC&quot;)
         .getOrCreate())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the watchdog, which is the part I would copy into any benchmark of your own. It measures the whole process tree, because JVMs and thread pools do not show up in the parent's RSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import psutil, subprocess, time

GIB = 1024 ** 3

def tree_rss(p):
    total = 0
    for q in [p] + p.children(recursive=True):
        try:
            total += q.memory_info().rss
        except psutil.NoSuchProcess:
            pass
    return total

proc = subprocess.Popen(cmd)
p, peak = psutil.Process(proc.pid), 0
while proc.poll() is None:
    rss = tree_rss(p)
    peak = max(peak, rss)
    if rss &amp;gt; 4.5 * GIB:
        for q in p.children(recursive=True) + [p]:
            q.kill()
        break
    time.sleep(0.05)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h3&gt;
&lt;p&gt;On one 8 vCPU machine with a 4.5 GiB cap, DuckDB 2.0.0 was the only engine that finished every run of every query at every size, and it did the heavy ones with 1 GB. Polars matched or beat it on the light queries and the group by, and could not finish the window dedup at 150M even with 9 GiB. Local Spark finished almost everything, 5x to 12x slower, and its memory setting did not bound its memory.&lt;/p&gt;
&lt;p&gt;If your job fits on one machine, measure it on one machine before you start a cluster for it.&lt;/p&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/2026/10/21/announcing-duckdb-200&quot;&gt;Announcing DuckDB 2.0.0 LTS &quot;Cinnamon Teal&quot;&lt;/a&gt;, DuckDB team, October 21, 2026&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/release_calendar&quot;&gt;DuckDB release calendar&lt;/a&gt; (2.0.0 LTS on 2026-10-21, 1.5.6 on 2026-09-28)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/duckdb/2.0.0/&quot;&gt;duckdb 2.0.0 on PyPI&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/polars/2.0.0/&quot;&gt;polars 2.0.0 on PyPI&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/pyspark/4.2.0/&quot;&gt;pyspark 4.2.0 on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads&quot;&gt;DuckDB: How to Tune Workloads&lt;/a&gt; (larger-than-memory operators, &lt;code&gt;preserve_insertion_order&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/docs/current/sql/dialect/order_preservation&quot;&gt;DuckDB: Order Preservation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;All timings and memory numbers: my own runs on October 19 (DuckDB 1.5.6, and the 9 GiB Polars and Spark runs) and October 22, 2026 (DuckDB 2.0.0, Polars 2.0.0, PySpark 4.2.0).&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32051/duckdb-2-0-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-4-5-gib-cap</guid>
<pubDate>Thu, 22 Oct 2026 08:52:00 +0000</pubDate>
</item>
<item>
<title>How I Automated GA4 &amp; GSC Reports for 5 Websites in Under 5 Minutes</title>
<link>https://coderlegion.com/32030/how-i-automated-ga4-gsc-reports-for-5-websites-in-under-5-minutes</link>
<description>&lt;p&gt;Creating monthly website performance reports can be surprisingly time-consuming, especially when you're managing multiple websites.&lt;/p&gt;
&lt;p&gt;You need to check Google Search Console, open Google Analytics 4, compare performance, organize the data, build charts, and figure out what actions to take next.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I wanted to simplify that process.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So I started using a Python-based reporting workflow to generate monthly website performance reports using Google Search Console (GSC) and Google Analytics 4 (GA4).&lt;/p&gt;
&lt;p&gt;The result? I can prepare reports for three websites in under five minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Does Each Report Include?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The automation generates a separate PDF report for each website.&lt;/p&gt;
&lt;p&gt;Each report contains:&lt;/p&gt;
&lt;p&gt;Google Search Console data: Organic search clicks, impressions, CTR, and average position.&lt;/p&gt;
&lt;p&gt;Google Analytics 4 data: Website traffic and engagement metrics.&lt;/p&gt;
&lt;p&gt;Performance tables: Organized data to make monthly comparisons easier.&lt;/p&gt;
&lt;p&gt;Visual charts: Graphs that help reveal traffic and search performance trends.&lt;/p&gt;
&lt;p&gt;Actionable recommendations: Suggested next steps based on each website's performance.&lt;/p&gt;
&lt;p&gt;Instead of spending most of my time collecting data, I can spend more time understanding what needs improvement.&lt;/p&gt;
&lt;p&gt;My Monthly Reporting Workflow&lt;/p&gt;
&lt;p&gt;Here's how I approach the process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Connect the Data Sources&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reporting workflow uses Google Search Console and Google Analytics 4 as its primary data sources.&lt;/p&gt;
&lt;p&gt;Each website needs access to the appropriate GSC property and GA4 property.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Run the Reporting Automation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once everything is configured, I run the reporting script.&lt;/p&gt;
&lt;p&gt;The workflow collects the available performance data and processes it for each website.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Generate a PDF Report&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The results are organized into 2 page PDF file containing tables, charts, and performance summaries.&lt;/p&gt;
&lt;p&gt;This makes it easier to review each website individually or share the reports with clients and team members.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Review the Recommended Actions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What should I prioritize next month?&lt;/p&gt;
&lt;p&gt;The reports provide suggested actions, which I review before making SEO decisions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I Prefer Automated Reports&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The biggest benefit isn't just saving time.&lt;/p&gt;
&lt;p&gt;It's having a repeatable workflow that makes monthly reporting more consistent.&lt;/p&gt;
&lt;p&gt;Instead of manually preparing similar reports every month, I can focus on analyzing trends, identifying problems, and planning improvements.&lt;/p&gt;
&lt;p&gt;Of course, automation doesn't replace human judgment. GA4 and GSC measure different things, and recommendations still need to be evaluated in context.&lt;/p&gt;
&lt;p&gt;Want to Build a Similar Reporting Workflow?&lt;/p&gt;
&lt;p&gt;I documented the approach in a tutorial on WiseTechList.&lt;/p&gt;
&lt;p&gt;Read the full guide:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wisetechlist.com/how-to-automate-ga4-gsc-report/&quot;&gt;How to Automate Google Analytics 4 and Google Search Console Reports&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can also explore more practical SEO, website analytics, automation, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://wisetechlist.com/research/&quot;&gt;software resources at WiseTechList.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you manage multiple websites, automating your reporting process is one practical way to spend less time preparing spreadsheets and more time improving results.&lt;/p&gt;
&lt;p&gt;What tools do you currently use for monthly SEO reporting?&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/32030/how-i-automated-ga4-gsc-reports-for-5-websites-in-under-5-minutes</guid>
<pubDate>Thu, 22 Oct 2026 05:29:31 +0000</pubDate>
</item>
<item>
<title>How to Build a Search System That Handles Thousands of Records</title>
<link>https://coderlegion.com/32012/how-to-build-a-search-system-that-handles-thousands-of-records</link>
<description>&lt;p&gt;Search functionality is one of the most important features in modern web applications. Whether you are building an e-commerce platform, a customer relationship management (CRM) system, an employee management dashboard, or a content management system, users expect to find the information they need quickly and accurately.&lt;/p&gt;
&lt;p&gt;At first, building a search feature might seem simple. You can retrieve records from a database, filter them based on a keyword, and display the matching results. However, as the application grows and the number of records reaches thousands or even hundreds of thousands, inefficient search logic can create performance problems.&lt;/p&gt;
&lt;p&gt;Users may experience slow response times, irrelevant results, excessive database queries, and frustrating pagination behavior. Developers must therefore design search systems that balance speed, accuracy, scalability, and maintainability.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to build a search system capable of handling thousands of records using React, Python, FastAPI, and PostgreSQL. We will cover database design, query optimization, pagination, filtering, indexing, frontend integration, error handling, testing, and future scalability.&lt;/p&gt;
&lt;p&gt;The goal is not simply to make search work. It is to build a reliable search feature that performs well as the application grows.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-the-requirements-of-a-scalable-search-system&quot;&gt;1. Understanding the Requirements of a Scalable Search System&lt;/h3&gt;
&lt;p&gt;Before writing code, it is important to understand what the search system needs to accomplish.&lt;/p&gt;
&lt;p&gt;Imagine developing a CRM application that stores 50,000 customer records. Each customer has a name, email address, phone number, company, and account status.&lt;/p&gt;
&lt;p&gt;A user might want to search for a customer by name, find a record using an email address, filter customers by status, or display only customers belonging to a particular company.&lt;/p&gt;
&lt;p&gt;A well-designed search system should support these requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keyword-based searching across selected fields.&lt;/li&gt;
&lt;li&gt;Filtering by status, category, date, or other attributes.&lt;/li&gt;
&lt;li&gt;Sorting by name, creation date, or relevance.&lt;/li&gt;
&lt;li&gt;Pagination to return manageable groups of results.&lt;/li&gt;
&lt;li&gt;Fast database queries through appropriate indexing.&lt;/li&gt;
&lt;li&gt;Input validation and safe query construction.&lt;/li&gt;
&lt;li&gt;Clear loading, empty, and error states in the frontend.&lt;/li&gt;
&lt;li&gt;Consistent results when users combine multiple filters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Performance should be considered from the beginning. Returning thousands of records in one HTTP response is usually unnecessary, even if the database can retrieve them quickly.&lt;/p&gt;
&lt;p&gt;Instead, the backend should return only the results required for the current page.&lt;/p&gt;
&lt;h3 id=&quot;2-choosing-the-right-technology-stack-1&quot;&gt;2. Choosing the Right Technology Stack&lt;/h3&gt;
&lt;p&gt;For this implementation, we will use React for the frontend, FastAPI for the backend, and PostgreSQL as the database.&lt;/p&gt;
&lt;p&gt;Each technology serves a different purpose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; provides the search interface, including input fields, filter controls, sorting options, and result tables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FastAPI&lt;/strong&gt; handles HTTP requests, validates query parameters, applies business rules, and communicates with the database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; stores customer records and executes search queries. Its indexing capabilities and full-text search features make it suitable for many applications that need more than basic filtering.&lt;/p&gt;
&lt;p&gt;This architecture separates responsibilities between the frontend, backend, and database.&lt;/p&gt;
&lt;p&gt;A typical request follows this sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user enters a keyword in the React interface.&lt;/li&gt;
&lt;li&gt;React sends a request to the FastAPI endpoint.&lt;/li&gt;
&lt;li&gt;FastAPI validates the keyword, filters, and pagination parameters.&lt;/li&gt;
&lt;li&gt;PostgreSQL executes an optimized query.&lt;/li&gt;
&lt;li&gt;FastAPI returns the matching records and pagination metadata.&lt;/li&gt;
&lt;li&gt;React renders the results.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This structure is easy to maintain and can be extended as the application becomes more complex.&lt;/p&gt;
&lt;p&gt;For smaller projects, PostgreSQL may be sufficient for the entire search workload. Dedicated search platforms such as Elasticsearch or OpenSearch can be introduced later if requirements justify the additional infrastructure.&lt;/p&gt;
&lt;h3 id=&quot;3-designing-the-database-schema&quot;&gt;3. Designing the Database Schema&lt;/h3&gt;
&lt;p&gt;A scalable search feature begins with a sensible database schema.&lt;/p&gt;
&lt;p&gt;For our CRM application, we can create a customers table containing the information required by the search interface.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE customers (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(150) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    company VARCHAR(150),
    status VARCHAR(30) NOT NULL DEFAULT 'active',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This schema includes a unique identifier, customer name, email address, company name, status, and creation timestamp.&lt;/p&gt;
&lt;p&gt;The primary key allows each record to be identified efficiently. The unique email constraint prevents duplicate email addresses and creates an index that supports email lookups.&lt;/p&gt;
&lt;p&gt;However, not every searchable field needs its own index. Indexes consume storage and increase the cost of inserts, updates, and deletes. We should create indexes based on actual query patterns.&lt;/p&gt;
&lt;p&gt;For example, if users frequently filter customers by status and sort them by creation date, a composite index may be useful:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_customers_status_created
ON customers (status, created_at DESC, id DESC);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This index can help PostgreSQL retrieve records in the desired order when the query filters by status. The &lt;code&gt;id&lt;/code&gt; column also provides a stable tie-breaker for pagination.&lt;/p&gt;
&lt;p&gt;Database design should reflect how users search rather than simply indexing every column.&lt;/p&gt;
&lt;h3 id=&quot;4-building-the-search-api-with-fastapi&quot;&gt;4. Building the Search API with FastAPI&lt;/h3&gt;
&lt;p&gt;The backend API is responsible for receiving search requests and returning structured results.&lt;/p&gt;
&lt;p&gt;We can create an endpoint such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/customers?q=alex&amp;amp;page=1&amp;amp;page_size=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;q&lt;/code&gt; parameter represents the search keyword. The &lt;code&gt;page&lt;/code&gt; parameter specifies the current page, while &lt;code&gt;page_size&lt;/code&gt; determines how many records to return.&lt;/p&gt;
&lt;p&gt;A response might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;items&quot;: [
    {
      &quot;id&quot;: 12,
      &quot;name&quot;: &quot;Alex Morgan&quot;,
      &quot;email&quot;: &quot;*Emails are not allowed*&quot;,
      &quot;company&quot;: &quot;Northstar&quot;,
      &quot;status&quot;: &quot;active&quot;
    }
  ],
  &quot;page&quot;: 1,
  &quot;page_size&quot;: 20,
  &quot;total&quot;: 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;items&lt;/code&gt; property contains the matching records. The remaining fields help the frontend display pagination controls and communicate the number of matches.&lt;/p&gt;
&lt;p&gt;For the implementation, SQLAlchemy can manage database interactions while Pydantic models validate API responses.&lt;/p&gt;
&lt;p&gt;First, define a response model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import datetime
from pydantic import BaseModel, ConfigDict


class CustomerOut(BaseModel):
    id: int
    name: str
    email: str
    company: str | None
    status: str
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)


class CustomerSearchResponse(BaseModel):
    items: list[CustomerOut]
    page: int
    page_size: int
    total: int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, implement the search endpoint using an asynchronous SQLAlchemy session:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import FastAPI, Depends, Query
from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI()


@app.get(
    &quot;/api/customers&quot;,
    response_model=CustomerSearchResponse
)
async def search_customers(
    q: str = Query(default=&quot;&quot;, max_length=100),
    status: str | None = Query(default=None),
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=20, ge=1, le=100),
    session: AsyncSession = Depends(get_session),
):
    filters = []

    keyword = q.strip()

    if keyword:
        pattern = f&quot;%{keyword}%&quot;

        filters.append(
            or_(
                Customer.name.ilike(pattern),
                Customer.email.ilike(pattern),
                Customer.company.ilike(pattern),
            )
        )

    if status:
        filters.append(Customer.status == status)

    count_query = (
        select(func.count())
        .select_from(Customer)
        .where(*filters)
    )

    total = await session.scalar(count_query)

    data_query = (
        select(Customer)
        .where(*filters)
        .order_by(
            Customer.created_at.desc(),
            Customer.id.desc()
        )
        .offset((page - 1) * page_size)
        .limit(page_size)
    )

    result = await session.scalars(data_query)
    items = result.all()

    return {
        &quot;items&quot;: items,
        &quot;page&quot;: page,
        &quot;page_size&quot;: page_size,
        &quot;total&quot;: total or 0,
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example assumes that &lt;code&gt;Customer&lt;/code&gt; is a mapped SQLAlchemy model and &lt;code&gt;get_session&lt;/code&gt; is a properly configured asynchronous session dependency.&lt;/p&gt;
&lt;p&gt;The query is constructed using SQLAlchemy expressions rather than concatenating user input into raw SQL. This helps prevent SQL injection through the search parameters.&lt;/p&gt;
&lt;p&gt;The example also limits the page size to 100 records. Such limits protect the API from unnecessarily large responses.&lt;/p&gt;
&lt;p&gt;In a production application, we should additionally validate the permitted status values, configure database connection pooling, handle expected database errors, and enforce authorization where customer data is restricted.&lt;/p&gt;
&lt;h3 id=&quot;5-understanding-partial-match-search&quot;&gt;5. Understanding Partial-Match Search&lt;/h3&gt;
&lt;p&gt;One of the most common search requirements is partial matching.&lt;/p&gt;
&lt;p&gt;Suppose the database contains these customer names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alexander Johnson&lt;/li&gt;
&lt;li&gt;Alexandra Smith&lt;/li&gt;
&lt;li&gt;Alex Morgan&lt;/li&gt;
&lt;li&gt;Benjamin Carter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a user enters &lt;code&gt;Alex&lt;/code&gt;, the search system should return the relevant records containing that text.&lt;/p&gt;
&lt;p&gt;In PostgreSQL, the &lt;code&gt;ILIKE&lt;/code&gt; operator supports case-insensitive pattern matching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, name, email
FROM customers
WHERE name ILIKE '%alex%';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The percent signs indicate that other characters may appear before or after the keyword.&lt;/p&gt;
&lt;p&gt;This is useful for flexible search, but there is an important performance consideration: a normal B-tree index generally cannot efficiently support a leading-wildcard pattern such as &lt;code&gt;%alex%&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With thousands of records, a sequential scan may still be acceptable. With much larger tables or more complex queries, the database may need a better indexing strategy.&lt;/p&gt;
&lt;p&gt;We should also distinguish between different types of search:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exact matching&lt;/strong&gt; finds a specific value, such as an email address.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prefix matching&lt;/strong&gt; finds values beginning with the keyword, such as names starting with &lt;code&gt;Alex&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Substring matching&lt;/strong&gt; finds text appearing anywhere within a field.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full-text search&lt;/strong&gt; finds documents based on words, linguistic processing, and relevance.&lt;/p&gt;
&lt;p&gt;Choosing the appropriate search method improves both performance and user experience.&lt;/p&gt;
&lt;h3 id=&quot;6-optimizing-search-with-database-indexes&quot;&gt;6. Optimizing Search with Database Indexes&lt;/h3&gt;
&lt;p&gt;Indexes are among the most important tools for improving database performance.&lt;/p&gt;
&lt;p&gt;Without a suitable index, PostgreSQL may need to inspect many rows to find matching records. As a table grows, this can increase query execution time.&lt;/p&gt;
&lt;p&gt;For exact email searches, the unique constraint already creates an appropriate index.&lt;/p&gt;
&lt;p&gt;For status-based filtering, an index can reduce the amount of data PostgreSQL needs to examine.&lt;/p&gt;
&lt;p&gt;For case-insensitive substring searches, PostgreSQL's &lt;code&gt;pg_trgm&lt;/code&gt; extension can support efficient trigram-based indexing.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX idx_customers_name_trgm
ON customers
USING GIN (name gin_trgm_ops);

CREATE INDEX idx_customers_email_trgm
ON customers
USING GIN (email gin_trgm_ops);

CREATE INDEX idx_customers_company_trgm
ON customers
USING GIN (company gin_trgm_ops);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trigram indexing breaks text into groups of three characters and can accelerate many substring and similarity searches.&lt;/p&gt;
&lt;p&gt;These indexes can be useful for queries involving &lt;code&gt;ILIKE '%keyword%'&lt;/code&gt;, although effectiveness depends on the query and the search term. Very short search terms may not benefit as much.&lt;/p&gt;
&lt;p&gt;Indexes also have trade-offs. Every additional index requires disk space and adds work to data modifications. Therefore, index selection should be based on real usage patterns and query plans.&lt;/p&gt;
&lt;p&gt;Use PostgreSQL's &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; to inspect actual execution behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN ANALYZE
SELECT id, name, email
FROM customers
WHERE name ILIKE '%alex%'
ORDER BY created_at DESC, id DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps developers identify sequential scans, expensive sorting operations, row estimates, and other performance bottlenecks.&lt;/p&gt;
&lt;p&gt;Rather than adding indexes blindly, measure the query before and after each change.&lt;/p&gt;
&lt;h3 id=&quot;7-implementing-pagination-correctly&quot;&gt;7. Implementing Pagination Correctly&lt;/h3&gt;
&lt;p&gt;Pagination prevents the backend from returning every matching record in a single response.&lt;/p&gt;
&lt;p&gt;Imagine that the CRM system contains 20,000 customers. Displaying all of them in one table would consume unnecessary memory, increase network transfer time, and make the interface difficult to use.&lt;/p&gt;
&lt;p&gt;With page-based pagination, the application can retrieve 20 records at a time.&lt;/p&gt;
&lt;p&gt;The SQL query uses &lt;code&gt;LIMIT&lt;/code&gt; and &lt;code&gt;OFFSET&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, name, email
FROM customers
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 40;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query retrieves up to 20 records after skipping the first 40. It corresponds to the third page when the page size is 20.&lt;/p&gt;
&lt;p&gt;Offset pagination is simple and appropriate for many administrative dashboards. However, large offsets can become expensive because the database may still need to walk through rows that will not be returned.&lt;/p&gt;
&lt;p&gt;Another issue is data consistency. If records are inserted or deleted while someone moves between pages, users may see duplicated or missing entries.&lt;/p&gt;
&lt;p&gt;For deeper result sets, cursor-based pagination is often a better option.&lt;/p&gt;
&lt;p&gt;A cursor can use the last record's timestamp and ID to fetch the next group:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, name, email, created_at
FROM customers
WHERE
    created_at &amp;lt; :last_created_at
    OR (
        created_at = :last_created_at
        AND id &amp;lt; :last_id
    )
ORDER BY created_at DESC, id DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example assumes descending ordering and a cursor containing the last record's timestamp and ID.&lt;/p&gt;
&lt;p&gt;In production, the cursor should be validated and encoded appropriately. The complete query must also account for any active search filters.&lt;/p&gt;
&lt;p&gt;Offset pagination works well when users need numbered pages and total counts. Cursor pagination is useful for large result sets, infinite scrolling, and activity feeds.&lt;/p&gt;
&lt;h3 id=&quot;8-building-the-search-interface-with-react&quot;&gt;8. Building the Search Interface with React&lt;/h3&gt;
&lt;p&gt;The frontend should make searching simple and predictable.&lt;/p&gt;
&lt;p&gt;A typical interface includes a search input, optional filters, a results table, pagination controls, and loading or error messages.&lt;/p&gt;
&lt;p&gt;React state can track the current keyword, result data, loading status, and errors.&lt;/p&gt;
&lt;p&gt;Here is a simplified example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useEffect, useState } from &quot;react&quot;;

export default function CustomerSearch() {
  const [query, setQuery] = useState(&quot;&quot;);
  const [customers, setCustomers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();

    async function search() {
      setLoading(true);
      setError(&quot;&quot;);

      try {
        const params = new URLSearchParams({
          q: query,
          page: &quot;1&quot;,
          page_size: &quot;20&quot;,
        });

        const response = await fetch(
          `/api/customers?${params.toString()}`,
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error(&quot;Unable to load customers.&quot;);
        }

        const data = await response.json();
        setCustomers(data.items);
      } catch (err) {
        if (err.name !== &quot;AbortError&quot;) {
          setError(err.message);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    search();

    return () =&amp;gt; controller.abort();
  }, [query]);

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;label htmlFor=&quot;customer-search&quot;&amp;gt;
        Search customers
      &amp;lt;/label&amp;gt;

      &amp;lt;input
        id=&quot;customer-search&quot;
        type=&quot;search&quot;
        value={query}
        onChange={(event) =&amp;gt; setQuery(event.target.value)}
        placeholder=&quot;Search by name, email, or company&quot;
      /&amp;gt;

      {loading &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Searching...&amp;lt;/p&amp;gt;}
      {error &amp;amp;&amp;amp; &amp;lt;p role=&quot;alert&quot;&amp;gt;{error}&amp;lt;/p&amp;gt;}

      {!loading &amp;amp;&amp;amp; !error &amp;amp;&amp;amp; customers.length === 0 &amp;amp;&amp;amp; (
        &amp;lt;p&amp;gt;No matching customers found.&amp;lt;/p&amp;gt;
      )}

      &amp;lt;ul&amp;gt;
        {customers.map((customer) =&amp;gt; (
          &amp;lt;li key={customer.id}&amp;gt;
            {customer.name} — {customer.email}
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component requests the first page of results whenever the search keyword changes. It also cancels outdated requests when a newer search begins or the component unmounts.&lt;/p&gt;
&lt;p&gt;That cancellation helps prevent unnecessary work and avoids displaying results from requests that are no longer relevant.&lt;/p&gt;
&lt;p&gt;The example assumes the API is available at the same origin. If the frontend and backend use different origins, the application must configure its API URL and appropriate CORS policies.&lt;/p&gt;
&lt;p&gt;For a full application, we should also implement pagination controls, status filters, sorting, and a consistent way to handle authentication.&lt;/p&gt;
&lt;h3 id=&quot;9-using-debouncing-to-reduce-api-requests&quot;&gt;9. Using Debouncing to Reduce API Requests&lt;/h3&gt;
&lt;p&gt;A common frontend performance problem occurs when the application sends a request after every keystroke.&lt;/p&gt;
&lt;p&gt;Suppose a user types &lt;code&gt;Alexander&lt;/code&gt;. Without debouncing, the frontend may send separate requests for &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;Al&lt;/code&gt;, &lt;code&gt;Ale&lt;/code&gt;, &lt;code&gt;Alex&lt;/code&gt;, and every subsequent character.&lt;/p&gt;
&lt;p&gt;Some of these requests may become unnecessary before they finish.&lt;/p&gt;
&lt;p&gt;Debouncing waits until the user stops typing for a short period before executing the search.&lt;/p&gt;
&lt;p&gt;A reusable React hook can implement this behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useEffect, useState } from &quot;react&quot;;

export function useDebouncedValue(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() =&amp;gt; {
    const timeoutId = setTimeout(() =&amp;gt; {
      setDebouncedValue(value);
    }, delay);

    return () =&amp;gt; clearTimeout(timeoutId);
  }, [value, delay]);

  return debouncedValue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The search component can then use the debounced keyword instead of the raw input value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const debouncedQuery = useDebouncedValue(query, 300);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;debouncedQuery&lt;/code&gt; in the API-request effect's dependency array and in the request parameters.&lt;/p&gt;
&lt;p&gt;This small change can significantly reduce the number of requests generated during typing.&lt;/p&gt;
&lt;p&gt;A delay of approximately 250–400 milliseconds is a reasonable starting point, but the ideal value depends on the application.&lt;/p&gt;
&lt;p&gt;Debouncing should not replace backend optimization. It reduces request frequency, while indexes, query planning, and pagination reduce the cost of each request.&lt;/p&gt;
&lt;h3 id=&quot;10-adding-filters-and-sorting&quot;&gt;10. Adding Filters and Sorting&lt;/h3&gt;
&lt;p&gt;Real-world search systems rarely depend on keywords alone.&lt;/p&gt;
&lt;p&gt;A CRM user might want to display only active customers, search for a company name, and sort the results by the newest creation date.&lt;/p&gt;
&lt;p&gt;The backend should allow these parameters to be combined.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/customers?q=alex&amp;amp;status=active&amp;amp;sort=created_at&amp;amp;direction=desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each filter should be validated independently and then combined with the search conditions.&lt;/p&gt;
&lt;p&gt;Sorting deserves special attention. Never insert arbitrary user-provided SQL column names or sort directions directly into a query.&lt;/p&gt;
&lt;p&gt;Instead, map supported parameters to known SQLAlchemy expressions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;sort_columns = {
    &quot;name&quot;: Customer.name,
    &quot;created_at&quot;: Customer.created_at,
    &quot;company&quot;: Customer.company,
}

sort_column = sort_columns.get(sort, Customer.created_at)

if direction == &quot;asc&quot;:
    ordering = sort_column.asc()
else:
    ordering = sort_column.desc()

data_query = data_query.order_by(
    ordering,
    Customer.id.desc()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;direction&lt;/code&gt; should be validated API parameters, with &lt;code&gt;sort&lt;/code&gt; restricted to the keys in the mapping and &lt;code&gt;direction&lt;/code&gt; restricted to &lt;code&gt;asc&lt;/code&gt; or &lt;code&gt;desc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The unique ID tie-breaker helps create deterministic ordering when multiple records have the same primary sort value.&lt;/p&gt;
&lt;p&gt;When users change a filter or sort order, the frontend should reset the current page to the first page. Otherwise, the selected page might no longer contain any results.&lt;/p&gt;
&lt;h3 id=&quot;11-improving-search-relevance&quot;&gt;11. Improving Search Relevance&lt;/h3&gt;
&lt;p&gt;Finding matching records is only one part of a useful search experience. The order of results matters too.&lt;/p&gt;
&lt;p&gt;Suppose someone searches for &lt;code&gt;*Emails are not allowed*&lt;/code&gt;. A record with that exact email should generally appear before a customer whose company description happens to contain the same keyword.&lt;/p&gt;
&lt;p&gt;One simple strategy is to prioritize exact matches, then prefix matches, followed by broader substring matches.&lt;/p&gt;
&lt;p&gt;PostgreSQL can express this with a ranking condition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, name, email
FROM customers
WHERE
    name ILIKE '%alex%'
    OR email ILIKE '%alex%'
    OR company ILIKE '%alex%'
ORDER BY
    CASE
        WHEN email = '*Emails are not allowed*' THEN 0
        WHEN name ILIKE 'alex%' THEN 1
        WHEN email ILIKE 'alex%' THEN 2
        ELSE 3
    END,
    created_at DESC,
    id DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a simplified illustration. In an actual application, the ranking rules should be built using parameterized query expressions, and the exact-match condition should be based on the original search input.&lt;/p&gt;
&lt;p&gt;For longer text documents, PostgreSQL full-text search offers additional capabilities.&lt;/p&gt;
&lt;p&gt;A basic full-text query might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, title
FROM documents
WHERE search_vector @@ websearch_to_tsquery('english', :query)
ORDER BY ts_rank(search_vector, websearch_to_tsquery('english', :query))
DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;search_vector&lt;/code&gt; represents a stored or generated &lt;code&gt;tsvector&lt;/code&gt; column that has been configured and indexed appropriately.&lt;/p&gt;
&lt;p&gt;Full-text search supports linguistic processing and relevance ranking, making it useful for knowledge bases, article directories, and documentation portals.&lt;/p&gt;
&lt;p&gt;However, full-text search and substring search solve different problems. If users need to find a partial email address or match characters in the middle of a product name, trigram indexing may be more appropriate.&lt;/p&gt;
&lt;h3 id=&quot;12-caching-frequently-requested-results&quot;&gt;12. Caching Frequently Requested Results&lt;/h3&gt;
&lt;p&gt;Caching can reduce repeated database work when many users search for the same information.&lt;/p&gt;
&lt;p&gt;For example, a dashboard may repeatedly request the same active customer list or a frequently used category of products.&lt;/p&gt;
&lt;p&gt;A cache such as Redis can store selected search responses for a short period.&lt;/p&gt;
&lt;p&gt;A cache key might include the normalized keyword, filters, sorting options, page number, and page size:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;customers:q=alex:status=active:sort=created_at:page=1:size=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every parameter that can change the result must be represented in the key. Otherwise, users might receive incorrect results from a cache entry created for a different search.&lt;/p&gt;
&lt;p&gt;Caching also introduces invalidation challenges. If a customer's status changes, a previously cached result may become outdated.&lt;/p&gt;
&lt;p&gt;Possible strategies include short expiration periods, targeted invalidation, and versioned cache keys.&lt;/p&gt;
&lt;p&gt;Caching should usually be introduced after measuring the system. For thousands of records, a well-indexed PostgreSQL database may already be fast enough. An unnecessary cache adds complexity, infrastructure costs, and another failure mode.&lt;/p&gt;
&lt;p&gt;Applications handling private or permission-sensitive data must also ensure that cache entries cannot leak results between users with different access rights.&lt;/p&gt;
&lt;h3 id=&quot;13-handling-errors-and-empty-results&quot;&gt;13. Handling Errors and Empty Results&lt;/h3&gt;
&lt;p&gt;A professional search experience should handle more than successful requests.&lt;/p&gt;
&lt;p&gt;Users may search for a keyword that has no matches, experience a network failure, or submit an invalid filter.&lt;/p&gt;
&lt;p&gt;The API should return predictable response formats and appropriate HTTP status codes.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;200 OK&lt;/code&gt; for a successful search, even when no records match.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;422 Unprocessable Entity&lt;/code&gt; for invalid FastAPI query parameters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt; when authentication is required but missing or invalid.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403 Forbidden&lt;/code&gt; when an authenticated user lacks permission.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt; when a rate limit is exceeded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Database outages and unexpected server errors should be logged for investigation, while the client receives a safe and understandable message.&lt;/p&gt;
&lt;p&gt;The frontend should distinguish between an empty result set and a failed request.&lt;/p&gt;
&lt;p&gt;A message such as &quot;No customers match your search&quot; is appropriate when the API returns an empty list. &quot;Unable to load customers. Please try again&quot; is more suitable when a request fails.&lt;/p&gt;
&lt;p&gt;Clear feedback helps users understand what happened and reduces unnecessary repeated actions.&lt;/p&gt;
&lt;h3 id=&quot;14-security-considerations&quot;&gt;14. Security Considerations&lt;/h3&gt;
&lt;p&gt;Search endpoints accept user input, so they require the same security attention as other API endpoints.&lt;/p&gt;
&lt;p&gt;First, use parameterized queries or a trusted query builder such as SQLAlchemy. Avoid building SQL statements through string concatenation.&lt;/p&gt;
&lt;p&gt;Second, validate query lengths, pagination limits, filter values, and sort options. A maximum keyword length can help prevent unusually large inputs from consuming excessive resources.&lt;/p&gt;
&lt;p&gt;Third, apply authentication and authorization wherever the records are not public. A user should only be able to search records they are allowed to access.&lt;/p&gt;
&lt;p&gt;For example, a multi-tenant CRM should scope its database query to the authenticated user's organization. Hiding unauthorized results only in React is not sufficient.&lt;/p&gt;
&lt;p&gt;Fourth, consider rate limiting. Automated clients can generate large numbers of expensive requests, even when each individual query is valid.&lt;/p&gt;
&lt;p&gt;Finally, do not log sensitive search terms or personal information unnecessarily. Logs should provide enough diagnostic information for troubleshooting while respecting privacy and data-retention requirements.&lt;/p&gt;
&lt;p&gt;Security and performance often reinforce each other. Restricting page sizes, validating parameters, and limiting request rates helps protect the system from accidental misuse as well as malicious traffic.&lt;/p&gt;
&lt;h3 id=&quot;15-testing-the-search-system&quot;&gt;15. Testing the Search System&lt;/h3&gt;
&lt;p&gt;A search feature should be tested with realistic data and a variety of query patterns.&lt;/p&gt;
&lt;p&gt;Unit tests can verify input validation, filter construction, sorting rules, and pagination calculations.&lt;/p&gt;
&lt;p&gt;Integration tests should run against a test database to confirm that the SQL queries return the expected records.&lt;/p&gt;
&lt;p&gt;Important test cases include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Searching for a complete customer name.&lt;/li&gt;
&lt;li&gt;Searching with different letter capitalization.&lt;/li&gt;
&lt;li&gt;Searching for a partial email address.&lt;/li&gt;
&lt;li&gt;Searching with leading or trailing whitespace.&lt;/li&gt;
&lt;li&gt;Applying a keyword and status filter together.&lt;/li&gt;
&lt;li&gt;Requesting an empty result set.&lt;/li&gt;
&lt;li&gt;Rejecting a page number below one.&lt;/li&gt;
&lt;li&gt;Rejecting an excessive page size.&lt;/li&gt;
&lt;li&gt;Testing keywords containing SQL-like input.&lt;/li&gt;
&lt;li&gt;Confirming stable ordering when records share the same timestamp.&lt;/li&gt;
&lt;li&gt;Verifying that one organization cannot access another organization's records.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Performance tests should use datasets that resemble actual production data. A test with 100 rows will not reveal the same bottlenecks as a test with 50,000 records.&lt;/p&gt;
&lt;p&gt;Measure latency at different levels, including database execution time, API response time, and the full user-perceived search experience.&lt;/p&gt;
&lt;p&gt;Useful metrics include median response time, p95 latency, request volume, error rate, database connection usage, and cache hit rate when caching is enabled.&lt;/p&gt;
&lt;p&gt;The objective is not merely to achieve a fast result once. It is to maintain predictable performance under normal and peak workloads.&lt;/p&gt;
&lt;h3 id=&quot;16-common-mistakes-to-avoid&quot;&gt;16. Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Several implementation mistakes can make a search feature difficult to scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loading every record into React:&lt;/strong&gt; Fetching thousands of rows and filtering them in the browser increases transfer time and memory usage. Perform filtering and pagination on the server when the dataset is substantial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Indexing every column:&lt;/strong&gt; Unnecessary indexes consume storage and slow down writes. Index the fields and combinations that match real query patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring query plans:&lt;/strong&gt; A query that appears efficient in development may behave differently with production data. Use &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; and representative datasets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sending requests for every keystroke:&lt;/strong&gt; Use debouncing and cancellation to avoid unnecessary requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using unstable sorting:&lt;/strong&gt; Ordering by a non-unique column can produce inconsistent page boundaries. Add a unique tie-breaker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using huge offsets for deep pagination:&lt;/strong&gt; Consider cursor pagination when navigating large result sets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adding caching too early:&lt;/strong&gt; Measure database performance first. Caching is valuable when it addresses a demonstrated bottleneck.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring access control:&lt;/strong&gt; Every query must enforce the correct data-access rules on the server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Returning unbounded results:&lt;/strong&gt; Always enforce sensible page-size limits and consider response-size constraints.&lt;/p&gt;
&lt;p&gt;Avoiding these mistakes creates a stronger foundation for future improvements.&lt;/p&gt;
&lt;h3 id=&quot;17-scaling-beyond-thousands-of-records&quot;&gt;17. Scaling Beyond Thousands of Records&lt;/h3&gt;
&lt;p&gt;A PostgreSQL-based search system can handle many workloads involving thousands or even millions of records when queries, indexes, and hardware are appropriate. However, the correct architecture depends on search complexity, update frequency, concurrency, and latency requirements.&lt;/p&gt;
&lt;p&gt;As the application grows, several improvements may become useful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connection pooling:&lt;/strong&gt; Reuse database connections efficiently rather than opening a new connection for every request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Query optimization:&lt;/strong&gt; Remove unnecessary joins, select only required columns, and optimize the most expensive filters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read replicas:&lt;/strong&gt; For suitable workloads, replicas can distribute read traffic, although replication lag and consistency requirements must be considered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dedicated search engines:&lt;/strong&gt; Elasticsearch or OpenSearch can support advanced text analysis, fuzzy matching, complex relevance ranking, and large-scale search workloads. They also introduce operational overhead and synchronization requirements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Asynchronous indexing:&lt;/strong&gt; If a dedicated search engine is used, database changes must be propagated reliably. Queues, change-data capture, or an outbox pattern can help prevent search indexes from becoming inconsistent with the source database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring and alerting:&lt;/strong&gt; Track slow queries, database saturation, API latency, and error rates so that performance problems are detected before they affect many users.&lt;/p&gt;
&lt;p&gt;These improvements should be introduced according to actual requirements rather than assumptions about future scale.&lt;/p&gt;
&lt;p&gt;For many business applications, a carefully indexed PostgreSQL database remains the simplest and most maintainable solution.&lt;/p&gt;
&lt;h3 id=&quot;18-a-practical-development-workflow&quot;&gt;18. A Practical Development Workflow&lt;/h3&gt;
&lt;p&gt;When implementing a search system, following a structured workflow helps reduce unnecessary complexity.&lt;/p&gt;
&lt;p&gt;Start by identifying the fields users need to search, the filters they require, and the expected number of records.&lt;/p&gt;
&lt;p&gt;Design the database schema and create a small but realistic dataset. Build a basic API that supports keyword search, validation, and pagination.&lt;/p&gt;
&lt;p&gt;Next, integrate the API with React. Add debouncing, loading indicators, empty states, and error handling.&lt;/p&gt;
&lt;p&gt;Once the basic functionality works, examine the database query plan and add appropriate indexes. Test the combined behavior of keyword search, filtering, sorting, and pagination.&lt;/p&gt;
&lt;p&gt;Finally, measure performance under realistic conditions. Add caching, cursor pagination, or a dedicated search engine only when the evidence shows that they are needed.&lt;/p&gt;
&lt;p&gt;This workflow keeps the initial implementation focused while leaving room for future improvements.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-5&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a search system that handles thousands of records is not simply a matter of adding a search box to a web application. It requires thoughtful database design, efficient queries, controlled pagination, a responsive frontend, and reliable error handling.&lt;/p&gt;
&lt;p&gt;React provides an intuitive search interface, FastAPI offers a structured backend for validating requests and applying business logic, and PostgreSQL provides powerful querying and indexing capabilities.&lt;/p&gt;
&lt;p&gt;By using appropriate indexes, debouncing requests, validating filters, implementing stable pagination, and measuring real query performance, developers can create a search experience that remains responsive as data grows.&lt;/p&gt;
&lt;p&gt;The most important lesson is to optimize based on evidence. Start with a simple, well-structured implementation, measure how it performs, identify bottlenecks, and introduce more advanced tools only when they solve a real problem.&lt;/p&gt;
&lt;p&gt;A scalable search system is not necessarily the one with the most technologies. It is the one that delivers accurate results efficiently, protects user data, and remains easy to maintain as the application evolves.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32012/how-to-build-a-search-system-that-handles-thousands-of-records</guid>
<pubDate>Thu, 22 Oct 2026 02:51:22 +0000</pubDate>
</item>
<item>
<title>Flip One Field: A Fairness Test That Beats Aggregate Parity</title>
<link>https://coderlegion.com/32004/flip-one-field-a-fairness-test-that-beats-aggregate-parity</link>
<description>&lt;p&gt;Most teams check an LLM-assisted decision system for fairness by comparing approval rates across groups. That check can pass cleanly while the model is still changing its mind on individual cases for reasons nobody would defend out loud. Recent fairness-stress work on LLM decision-making points at a better default test - and it's a decision you can make before writing any code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Aggregate Parity Nets Out; Per-Case Instability Doesn't&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The choice is between two measurements. &lt;strong&gt;Outcome parity&lt;/strong&gt; asks: across 1,000 applications, did group A get approved at roughly the same rate as group B? &lt;strong&gt;Counterfactual stress&lt;/strong&gt; asks something narrower and harder: for &lt;em&gt;this one&lt;/em&gt; application, if I change only a decision-irrelevant field - the name, the phrasing of the same request, the order of the bullet points - does the answer move?&lt;/p&gt;
&lt;p&gt;A model that favors one group on early-career cases and another on senior cases can show perfect parity overall. Parity is cheap and it's what regulators often ask for, but it averages. Per-case stress testing catches that, because it never aggregates across people - it compares a case to itself.&lt;/p&gt;
&lt;p&gt;The trade-off is real. Stress testing costs N× the inference calls, and it requires someone to declare which fields are decision-irrelevant. That list is a policy judgment, not an engineering one, which is exactly why a PM should define it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Running the Test on One Application&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;base = {&quot;years_exp&quot;: 6, &quot;skills&quot;: &quot;python, sql&quot;}
names = [&quot;Alex Chen&quot;, &quot;Aisha Khan&quot;, &quot;John Smith&quot;, &quot;Priya Patel&quot;]

votes = [decide(tmpl, {**base, &quot;name&quot;: n})
 for n in names
 for tmpl in PROMPT_TEMPLATES] # 3 paraphrases, same instruction

flip_rate = 1 - max(votes.count(v) for v in set(votes)) / len(votes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Twelve variants of one application that a human reviewer would call identical. Any &lt;code&gt;flip_rate&lt;/code&gt; above zero means the decision moved on something that shouldn't matter.&lt;/p&gt;
&lt;p&gt;The number alone isn't the point - the &lt;em&gt;pattern&lt;/em&gt; is. Read the flips two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clustered&lt;/strong&gt; - all rejections land on the same name group across all three paraphrases. That's bias. Fix it in the prompt, the retrieval data, or the model choice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scattered&lt;/strong&gt; - flips appear randomly across names and templates. That's instability. Lower the temperature, constrain the output format, or stop auto-deciding borderline cases.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both fail the same test and need different fixes, which is why one aggregate percentage can't tell you what to do next. Report worst-case flip rate per application, not the mean - the mean hides the cases that would end up in a complaint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Group-level parity can look clean while individual decisions flip on irrelevant details; counterfactual stress testing compares each case against itself and catches what averages hide.&lt;/li&gt;
&lt;li&gt;Separate clustered flips (bias - fix prompt, data, or model) from scattered flips (instability - fix sampling, output constraints, or add human review).&lt;/li&gt;
&lt;li&gt;The hard part isn't the code, it's agreeing on which input fields are decision-irrelevant. That list belongs to product and policy, not just engineering.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which fields in your decision prompt would your team actually agree are irrelevant - and how long would that meeting take?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sources referenced: FairStress: A deeper evaluation of fairness in LLM decision-making, Hugging Face Blog&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32004/flip-one-field-a-fairness-test-that-beats-aggregate-parity</guid>
<pubDate>Wed, 21 Oct 2026 21:04:43 +0000</pubDate>
</item>
<item>
<title>Build a Python agent harness you can extend and test without calling a model</title>
<link>https://coderlegion.com/31972/build-a-python-agent-harness-you-can-extend-and-test-without-calling-a-model</link>
<description>&lt;p&gt;A team ships a small Python agent with two tools. A month later someone renames an argument on one tool function and forgets the hand-written JSON schema sitting a few lines above it. The model keeps sending the old argument name, the function raises on every call, and nobody finds out until a real task hits that tool, because the only way anyone ever tested the loop was by running it against a live model.&lt;/p&gt;
&lt;p&gt;The layout caused this one. When tool contracts are written by hand and the loop only runs end to end, every change to a tool risks the whole agent, and every test costs an API call. A harness you can live with keeps three things apart: the loop, the tool contracts, and the tests that prove the loop behaves.&lt;/p&gt;
&lt;h4 id=&quot;the-loop-has-three-parts-and-only-one-of-them-should-change-when-you-add-a-tool&quot;&gt;The loop has three parts, and only one of them should change when you add a tool&lt;/h4&gt;
&lt;p&gt;A minimal agent harness in Python has a loop over the model's &lt;code&gt;stop_reason&lt;/code&gt;, a dispatcher that maps a tool name to a Python function, and a &lt;code&gt;messages&lt;/code&gt; list that grows every turn. The loop keeps running while the model asks for tools (&lt;code&gt;stop_reason == &quot;tool_use&quot;&lt;/code&gt;) and returns the text answer when it sends &lt;code&gt;end_turn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Of those three, only the set of tools should grow as the agent gains abilities. The loop itself should not change when you add a tool, and the message list should follow the same rules on every turn. A production harness wraps more around this core, such as sandboxing, permission checks, context compaction and session state. Those layers are easier to add later when the core stays this small.&lt;/p&gt;
&lt;h4 id=&quot;generate-the-schema-from-the-function-s-input-type&quot;&gt;Generate the schema from the function's input type&lt;/h4&gt;
&lt;p&gt;Every tool needs a name, a description and an &lt;code&gt;input_schema&lt;/code&gt; that tells the model which arguments it accepts. Writing that JSON Schema by hand is how the opening bug happens. The schema and the function live in two places, and only one of them gets edited.&lt;/p&gt;
&lt;p&gt;Describe each tool's inputs with a Pydantic model and generate the schema from it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One model per tool.&lt;/strong&gt; A &lt;code&gt;ReadFile&lt;/code&gt; model with a single &lt;code&gt;path: str&lt;/code&gt; field describes the input of &lt;code&gt;read_file&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let the library write the schema.&lt;/strong&gt; &lt;code&gt;ReadFile.model_json_schema()&lt;/code&gt; returns a plain dict with &lt;code&gt;path&lt;/code&gt; as a required string, which is the &lt;code&gt;input_schema&lt;/code&gt; shape the Anthropic API validates against.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edit the type and the schema follows.&lt;/strong&gt; Add a field or mark one optional and the generated schema updates itself. This method ships with Pydantic v2.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Pydantic model becomes the single source of truth for a tool's inputs, and the function's arguments have to match its fields. The dispatch fixture in the testing section below fails the moment the two drift apart, long before a real task finds it.&lt;/p&gt;
&lt;h4 id=&quot;dispatch-by-name-so-the-loop-never-names-a-tool&quot;&gt;Dispatch by name so the loop never names a tool&lt;/h4&gt;
&lt;p&gt;Keep a dict called &lt;code&gt;HANDLERS&lt;/code&gt; that maps each tool name to its function, and dispatch with one line: &lt;code&gt;HANDLERS[block.name](**block.input)&lt;/code&gt;. The loop reads the tool name from the model's &lt;code&gt;tool_use&lt;/code&gt; block and never refers to a specific tool.&lt;/p&gt;
&lt;p&gt;That one decision is what makes the harness extendable. Adding a tool touches three places, and none of them is the loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Define the input model.&lt;/strong&gt; A &lt;code&gt;WriteFile&lt;/code&gt; model with &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; fields.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add one entry to the tool list.&lt;/strong&gt; Name, description, and &lt;code&gt;WriteFile.model_json_schema()&lt;/code&gt; as the schema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register the function.&lt;/strong&gt; &lt;code&gt;HANDLERS[&quot;write_file&quot;] = write_file&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agent can now write files as well as read them, and &lt;code&gt;run_agent&lt;/code&gt; did not change by a single character. If adding a tool ever forces an edit inside the loop, a tool name has leaked into it.&lt;/p&gt;
&lt;h4 id=&quot;treat-what-comes-back-as-part-of-the-contract&quot;&gt;Treat what comes back as part of the contract&lt;/h4&gt;
&lt;p&gt;The input side is only half of a tool's contract. The output has rules too. The &lt;code&gt;content&lt;/code&gt; of a &lt;code&gt;tool_result&lt;/code&gt; must be a string or a list of content blocks. A &lt;code&gt;read_file&lt;/code&gt; that returns a string passes straight through, while a &lt;code&gt;list_files&lt;/code&gt; that returns a Python list has to be serialized first, for example with &lt;code&gt;json.dumps&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Put that rule in the loop once, so no single tool has to remember it. The same goes for pairing: copy the &lt;code&gt;id&lt;/code&gt; from each &lt;code&gt;tool_use&lt;/code&gt; block into the result's &lt;code&gt;tool_use_id&lt;/code&gt;, and append the assistant message before the user message that carries the results. These are loop rules, and they belong next to the dispatch line, where every tool gets them for free.&lt;/p&gt;
&lt;h4 id=&quot;test-the-loop-without-calling-a-model&quot;&gt;Test the loop without calling a model&lt;/h4&gt;
&lt;p&gt;Testing a harness by running it against a live model and reading the output costs money, takes seconds per run, and only covers the path the model happened to take that time.&lt;/p&gt;
&lt;p&gt;The loop is ordinary Python, so test it like ordinary Python. Mock the &lt;code&gt;messages.create&lt;/code&gt; call to return fixtures, and assert on what the loop does with them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A tool-use fixture drives one turn.&lt;/strong&gt; Return a response with &lt;code&gt;stop_reason&lt;/code&gt; set to &lt;code&gt;tool_use&lt;/code&gt; and a fake &lt;code&gt;tool_use&lt;/code&gt; block, then check that the matching handler ran with the arguments in the block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The append order holds.&lt;/strong&gt; After that turn, the assistant message should sit before the user message that carries the result, and the result's &lt;code&gt;tool_use_id&lt;/code&gt; should match the block's &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parallel calls arrive together.&lt;/strong&gt; Return a fixture with two &lt;code&gt;tool_use&lt;/code&gt; blocks and assert that both results go back in a single user message before the next call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-string results get serialized.&lt;/strong&gt; Point a fake tool at a list return and check that the &lt;code&gt;content&lt;/code&gt; the loop sends back is a string.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The loop exits on &lt;code&gt;end_turn&lt;/code&gt;.&lt;/strong&gt; Follow the tool turn with an &lt;code&gt;end_turn&lt;/code&gt; fixture and check that &lt;code&gt;run_agent&lt;/code&gt; returns its text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The ceiling fires.&lt;/strong&gt; Feed &lt;code&gt;tool_use&lt;/code&gt; fixtures forever and check that the loop raises once it reaches &lt;code&gt;max_turns&lt;/code&gt;, instead of spinning.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One detail matters when you read the mock's recorded calls: &lt;code&gt;run_agent&lt;/code&gt; keeps appending to the same &lt;code&gt;messages&lt;/code&gt; list, so copy the list inside the mock if you want to assert on what each call saw. These tests run in milliseconds and never reach the API, and they cover the rules that break a harness without anyone noticing. Run them on every change, and keep the live-model run for checking behavior that only a model can show.&lt;/p&gt;
&lt;h4 id=&quot;know-what-a-framework-would-take-over&quot;&gt;Know what a framework would take over&lt;/h4&gt;
&lt;p&gt;The OpenAI Agents SDK runs the same loop for you through &lt;code&gt;Runner.run&lt;/code&gt;. You describe tools as Python functions with type annotations, and the SDK raises &lt;code&gt;MaxTurnsExceeded&lt;/code&gt; when a run passes its &lt;code&gt;max_turns&lt;/code&gt;, which is the same safety valve as the manual counter.&lt;/p&gt;
&lt;p&gt;Reach for the SDK when you need multi-agent handoffs, built-in guardrails or tracing. Keep the manual loop when you want zero framework dependency or need to see exactly what happens on every turn. OpenAI Swarm is an experimental, stateless project, and the Agents SDK has superseded it for anything you plan to run in production.&lt;/p&gt;
&lt;p&gt;Either way, the design rule carries over. Tool definitions stay separate from loop logic, and switching to a framework later holds no surprises in the control flow.&lt;/p&gt;
&lt;h4 id=&quot;pitfalls-that-keep-showing-up&quot;&gt;Pitfalls that keep showing up&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hand-written schema dicts.&lt;/strong&gt; They drift from the function the first time someone edits a signature.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool names inside the loop.&lt;/strong&gt; Every new tool turns into a change to the control flow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raw Python objects in &lt;code&gt;tool_result&lt;/code&gt;.&lt;/strong&gt; The API expects a string or content blocks, so a returned list fails validation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A missing &lt;code&gt;max_tokens&lt;/code&gt;.&lt;/strong&gt; It is required on every Anthropic request and has no default, so the SDK raises before the model ever runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live-model runs as the only test.&lt;/strong&gt; They are slow, they cost money, and they never force the edge cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The full program, with the tool definitions, the loop and the stop conditions in one file you can run, is in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://futureagi.com/blog/build-agent-harness-python/?utm_source=fagiCoderlegion&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution&quot;&gt;build guide with the full code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A loop that never names a tool, schemas generated from types and tests that need no model call together keep a harness small. Every new tool after that is a few lines next to the others.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31972/build-a-python-agent-harness-you-can-extend-and-test-without-calling-a-model</guid>
<pubDate>Wed, 21 Oct 2026 12:50:26 +0000</pubDate>
</item>
<item>
<title>Building a File Upload System with React and Python</title>
<link>https://coderlegion.com/31930/building-a-file-upload-system-with-react-and-python</link>
<description>&lt;p&gt;File uploads are an essential feature of modern web applications. Whether users are uploading profile pictures, submitting documents, sharing reports, attaching files to messages, or managing media in a SaaS platform, a reliable file upload system plays an important role in the overall user experience.&lt;/p&gt;
&lt;p&gt;Although uploading a file might seem like a simple task, building a production-ready system requires careful planning. Developers must consider frontend usability, backend validation, security, storage management, error handling, performance, and scalability.&lt;/p&gt;
&lt;p&gt;React provides an interactive frontend for selecting files, displaying upload progress, and communicating with backend APIs. Python, particularly with FastAPI, provides an efficient way to receive files, validate their contents, enforce upload limits, and store them securely.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to build a complete file upload system using React and Python. We will examine the architecture, implementation details, security considerations, and strategies for preparing the system for production.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-the-file-upload-architecture&quot;&gt;1. Understanding the File Upload Architecture&lt;/h3&gt;
&lt;p&gt;A file upload system typically consists of three major components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; React handles file selection, previews, validation feedback, and upload progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Python receives the uploaded file, performs validation, checks authorization, and manages the upload process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; Files are saved in local storage, a private object storage service, or another appropriate storage solution.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The communication between these components generally follows an HTTP request-response pattern.&lt;/p&gt;
&lt;p&gt;When a user selects a file, React creates a &lt;code&gt;FormData&lt;/code&gt; object and sends it to the backend using an HTTP POST request. The backend validates the request and stores the file. After successful processing, the API returns a response containing information about the uploaded file.&lt;/p&gt;
&lt;p&gt;A typical workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user selects a file.&lt;/li&gt;
&lt;li&gt;React performs basic client-side validation.&lt;/li&gt;
&lt;li&gt;The frontend sends the file using multipart form data.&lt;/li&gt;
&lt;li&gt;FastAPI checks authentication, authorization, file size, and permitted file types.&lt;/li&gt;
&lt;li&gt;The backend generates a safe storage name.&lt;/li&gt;
&lt;li&gt;The file is stored in the configured storage system.&lt;/li&gt;
&lt;li&gt;The API returns a success response.&lt;/li&gt;
&lt;li&gt;React updates the interface with the upload result.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This separation of responsibilities makes the application easier to maintain and extend.&lt;/p&gt;
&lt;h3 id=&quot;2-setting-up-the-python-backend&quot;&gt;2. Setting Up the Python Backend&lt;/h3&gt;
&lt;p&gt;For this implementation, we will use FastAPI because it provides straightforward request handling, automatic API documentation, and support for asynchronous web applications.&lt;/p&gt;
&lt;p&gt;First, create a project directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir file-upload-system
cd file-upload-system

python -m venv venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Activate the virtual environment.&lt;/p&gt;
&lt;p&gt;On Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Linux or macOS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the required dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install fastapi uvicorn python-multipart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The packages serve different purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fastapi&lt;/code&gt; provides the backend framework.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvicorn&lt;/code&gt; runs the application.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python-multipart&lt;/code&gt; enables multipart form-data parsing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create the following project structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;file-upload-system/
├── backend/
│   ├── main.py
│   └── uploads/
└── frontend/
    └── src/
        ├── App.jsx
        └── App.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;uploads&lt;/code&gt; directory will be used for local development. In a production deployment, private files should generally be stored outside publicly served application directories or in private object storage.&lt;/p&gt;
&lt;h3 id=&quot;3-building-the-file-upload-api-with-fastapi&quot;&gt;3. Building the File Upload API with FastAPI&lt;/h3&gt;
&lt;p&gt;The first step is to create an API endpoint that receives and stores uploaded files.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;backend/main.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pathlib import Path
from uuid import uuid4
import os

from fastapi import (
    FastAPI,
    File,
    HTTPException,
    UploadFile,
)
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(title=&quot;File Upload API&quot;)

BASE_DIR = Path(__file__).resolve().parent
UPLOAD_DIR = BASE_DIR / &quot;uploads&quot;
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)

MAX_FILE_SIZE = 5 * 1024 * 1024

ALLOWED_EXTENSIONS = {
    &quot;.pdf&quot;,
    &quot;.png&quot;,
    &quot;.jpg&quot;,
    &quot;.jpeg&quot;,
}

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        &quot;http://localhost:5173&quot;
    ],
    allow_credentials=True,
    allow_methods=[&quot;GET&quot;, &quot;POST&quot;],
    allow_headers=[&quot;*&quot;],
)


@app.post(&quot;/api/upload&quot;)
async def upload_file(
    file: UploadFile = File(...)
):
    original_name = Path(
        file.filename or &quot;&quot;
    ).name

    extension = Path(
        original_name
    ).suffix.lower()

    if not original_name:
        raise HTTPException(
            status_code=400,
            detail=&quot;A filename is required.&quot;
        )

    if extension not in ALLOWED_EXTENSIONS:
        raise HTTPException(
            status_code=400,
            detail=&quot;File type is not allowed.&quot;
        )

    file_id = uuid4().hex
    stored_name = f&quot;{file_id}{extension}&quot;
    destination = UPLOAD_DIR / stored_name

    total_size = 0

    try:
        with destination.open(&quot;xb&quot;) as output:
            while True:
                chunk = await file.read(1024 * 1024)

                if not chunk:
                    break

                total_size += len(chunk)

                if total_size &amp;gt; MAX_FILE_SIZE:
                    raise HTTPException(
                        status_code=413,
                        detail=&quot;File exceeds the 5 MB limit.&quot;
                    )

                output.write(chunk)

        return {
            &quot;message&quot;: &quot;File uploaded successfully.&quot;,
            &quot;file_id&quot;: file_id,
            &quot;filename&quot;: original_name,
            &quot;size&quot;: total_size,
            &quot;content_type&quot;: file.content_type,
        }

    except HTTPException:
        destination.unlink(missing_ok=True)
        raise

    except OSError:
        destination.unlink(missing_ok=True)
        raise HTTPException(
            status_code=500,
            detail=&quot;Could not save the uploaded file.&quot;
        )

    finally:
        await file.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint provides a basic foundation for handling uploads.&lt;/p&gt;
&lt;h4 id=&quot;how-the-backend-works&quot;&gt;How the backend works&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;File reception:&lt;/strong&gt; FastAPI uses &lt;code&gt;UploadFile&lt;/code&gt; to receive the uploaded file. Unlike reading the entire file into a Python byte string, &lt;code&gt;UploadFile&lt;/code&gt; uses a spooled temporary file, which is more suitable for many upload scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extension validation:&lt;/strong&gt; The code restricts uploads to PDF, PNG, JPG, and JPEG extensions. This helps enforce the application's expected file types, but it does not prove that the file actually contains valid content of that type.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unique storage names:&lt;/strong&gt; A UUID generates a unique storage identifier. This prevents users from overwriting files simply by uploading files with identical names.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Size enforcement:&lt;/strong&gt; The endpoint reads the file in chunks and stops when the configured size limit is exceeded.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cleanup:&lt;/strong&gt; If writing fails or the file exceeds the limit, the partially written destination is removed.&lt;/p&gt;
&lt;p&gt;This example is suitable for learning and local development. A public production API needs additional safeguards, including authentication, authorization, content inspection, and limits at the reverse proxy or application server.&lt;/p&gt;
&lt;h3 id=&quot;4-creating-the-react-frontend&quot;&gt;4. Creating the React Frontend&lt;/h3&gt;
&lt;p&gt;Now that the backend is available, we can build a React interface that allows users to select and upload files.&lt;/p&gt;
&lt;p&gt;A Vite-based React project can be created with the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create vite@latest frontend -- --template react
cd frontend
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the development server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create or replace &lt;code&gt;src/App.jsx&lt;/code&gt; with the following implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useState } from &quot;react&quot;;
import &quot;./App.css&quot;;

const MAX_FILE_SIZE = 5 * 1024 * 1024;

const ALLOWED_TYPES = [
  &quot;application/pdf&quot;,
  &quot;image/png&quot;,
  &quot;image/jpeg&quot;,
];

export default function App() {
  const [file, setFile] = useState(null);
  const [status, setStatus] = useState(&quot;&quot;);
  const [error, setError] = useState(&quot;&quot;);
  const [uploading, setUploading] = useState(false);
  const [result, setResult] = useState(null);

  function handleFileChange(event) {
    const selectedFile = event.target.files?.[0];

    setError(&quot;&quot;);
    setStatus(&quot;&quot;);
    setResult(null);

    if (!selectedFile) {
      setFile(null);
      return;
    }

    if (selectedFile.size &amp;gt; MAX_FILE_SIZE) {
      setError(&quot;Please select a file smaller than 5 MB.&quot;);
      setFile(null);
      return;
    }

    if (!ALLOWED_TYPES.includes(selectedFile.type)) {
      setError(&quot;Only PDF, PNG, and JPEG files are allowed.&quot;);
      setFile(null);
      return;
    }

    setFile(selectedFile);
  }

  async function handleUpload() {
    if (!file || uploading) return;

    setUploading(true);
    setError(&quot;&quot;);
    setStatus(&quot;&quot;);
    setResult(null);

    const formData = new FormData();
    formData.append(&quot;file&quot;, file);

    try {
      const response = await fetch(
        &quot;http://localhost:8000/api/upload&quot;,
        {
          method: &quot;POST&quot;,
          body: formData,
        }
      );

      const data = await response.json();

      if (!response.ok) {
        throw new Error(
          data.detail || &quot;Upload failed.&quot;
        );
      }

      setResult(data);
      setStatus(&quot;File uploaded successfully!&quot;);
      setFile(null);
    } catch (err) {
      setError(
        err.message || &quot;Unable to upload the file.&quot;
      );
    } finally {
      setUploading(false);
    }
  }

  return (
    &amp;lt;main className=&quot;upload-page&quot;&amp;gt;
      &amp;lt;section className=&quot;upload-card&quot;&amp;gt;
        &amp;lt;h1&amp;gt;File Upload System&amp;lt;/h1&amp;gt;

        &amp;lt;p&amp;gt;
          Select a PDF, PNG, or JPEG file
          up to 5 MB.
        &amp;lt;/p&amp;gt;

        &amp;lt;label htmlFor=&quot;file-input&quot;&amp;gt;
          Choose a file
        &amp;lt;/label&amp;gt;

        &amp;lt;input
          id=&quot;file-input&quot;
          type=&quot;file&quot;
          accept=&quot;.pdf,.png,.jpg,.jpeg&quot;
          onChange={handleFileChange}
        /&amp;gt;

        {file &amp;amp;&amp;amp; (
          &amp;lt;div className=&quot;file-info&quot;&amp;gt;
            &amp;lt;strong&amp;gt;{file.name}&amp;lt;/strong&amp;gt;
            &amp;lt;span&amp;gt;
              {(file.size / 1024).toFixed(2)} KB
            &amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
        )}

        &amp;lt;button
          type=&quot;button&quot;
          onClick={handleUpload}
          disabled={!file || uploading}
        &amp;gt;
          {uploading ? &quot;Uploading...&quot; : &quot;Upload File&quot;}
        &amp;lt;/button&amp;gt;

        {status &amp;amp;&amp;amp; (
          &amp;lt;p className=&quot;success&quot;&amp;gt;{status}&amp;lt;/p&amp;gt;
        )}

        {error &amp;amp;&amp;amp; (
          &amp;lt;p className=&quot;error&quot; role=&quot;alert&quot;&amp;gt;
            {error}
          &amp;lt;/p&amp;gt;
        )}

        {result &amp;amp;&amp;amp; (
          &amp;lt;div className=&quot;result&quot;&amp;gt;
            &amp;lt;h2&amp;gt;Upload Details&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;File: {result.filename}&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;File ID: {result.file_id}&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;Size: {result.size} bytes&amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
        )}
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;important-implementation-details&quot;&gt;Important implementation details&lt;/h4&gt;
&lt;p&gt;The frontend uses React state to keep track of the selected file, upload status, errors, and successful results.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FormData&lt;/code&gt; API prepares the request body in the format expected by FastAPI's &lt;code&gt;UploadFile&lt;/code&gt; parameter.&lt;/p&gt;
&lt;p&gt;Notice that the code does not manually set the &lt;code&gt;Content-Type&lt;/code&gt; header. The browser automatically generates the correct multipart boundary when sending a &lt;code&gt;FormData&lt;/code&gt; object. Setting this header manually can cause request parsing problems.&lt;/p&gt;
&lt;p&gt;The interface also disables the upload button while a request is in progress. This helps prevent accidental duplicate submissions.&lt;/p&gt;
&lt;p&gt;Client-side validation improves usability, but it must never replace server-side validation. Anyone can bypass browser restrictions and send requests directly to the API.&lt;/p&gt;
&lt;h3 id=&quot;5-styling-the-upload-interface&quot;&gt;5. Styling the Upload Interface&lt;/h3&gt;
&lt;p&gt;A clean interface helps users understand what they are uploading and whether the operation succeeded.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;src/App.css&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background: #f3f5f9;
  color: #1f2937;
}

.upload-page {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 24px;
}

.upload-card {
  width: 100%;
  max-width: 520px;
  padding: 32px;
  border-radius: 16px;
  background: white;
  box-shadow: 0 10px 35px rgba(0, 0, 0, 0.08);
}

.upload-card h1 {
  margin-top: 0;
  font-size: 28px;
}

.upload-card p {
  line-height: 1.6;
  color: #596579;
}

.upload-card input {
  display: block;
  width: 100%;
  margin: 16px 0;
}

.upload-card button {
  width: 100%;
  padding: 13px 18px;
  border: none;
  border-radius: 8px;
  background: #2563eb;
  color: white;
  font-size: 16px;
  cursor: pointer;
}

.upload-card button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.file-info,
.result {
  margin: 18px 0;
  padding: 14px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow-wrap: anywhere;
}

.file-info {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.success {
  color: #15803d !important;
}

.error {
  color: #b91c1c !important;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This responsive layout works well as a starting point for a small application. A more advanced interface could include drag-and-drop uploads, file previews, progress indicators, cancellation controls, and a history of uploaded files.&lt;/p&gt;
&lt;h3 id=&quot;6-running-the-complete-application&quot;&gt;6. Running the Complete Application&lt;/h3&gt;
&lt;p&gt;Open two terminal windows.&lt;/p&gt;
&lt;p&gt;In the first terminal, start the backend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd file-upload-system/backend

uvicorn main:app --reload --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the second terminal, start the React frontend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd file-upload-system/frontend

npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open the frontend URL displayed by Vite, usually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Select a supported file and click the upload button.&lt;/p&gt;
&lt;p&gt;If the upload succeeds, the frontend displays the original filename, generated file identifier, and file size. The stored file should appear in the backend's &lt;code&gt;uploads&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;FastAPI also provides interactive API documentation at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:8000/docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The documentation is useful for testing the upload endpoint independently of React.&lt;/p&gt;
&lt;h3 id=&quot;7-understanding-file-upload-security&quot;&gt;7. Understanding File Upload Security&lt;/h3&gt;
&lt;p&gt;File uploads introduce security risks because users can submit arbitrary content to an application. A secure system must treat uploaded files as untrusted input.&lt;/p&gt;
&lt;h4 id=&quot;validate-file-size-1&quot;&gt;Validate file size&lt;/h4&gt;
&lt;p&gt;Large uploads can consume memory, disk space, bandwidth, and processing capacity. Establish clear upload limits and enforce them on the server.&lt;/p&gt;
&lt;p&gt;For larger files, use streaming or multipart upload techniques rather than loading entire files into memory.&lt;/p&gt;
&lt;p&gt;The example reads the file in chunks, but FastAPI and its underlying multipart parser may already have processed or spooled request data before the endpoint begins reading it. Therefore, production deployments should also configure request-body limits at the proxy or gateway and test oversized requests.&lt;/p&gt;
&lt;h4 id=&quot;validate-actual-file-content&quot;&gt;Validate actual file content&lt;/h4&gt;
&lt;p&gt;File extensions and browser-supplied MIME types are not trustworthy on their own. A malicious user can rename an executable or another unexpected file with a &lt;code&gt;.jpg&lt;/code&gt; extension.&lt;/p&gt;
&lt;p&gt;Depending on the supported file types, stronger validation can include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Checking file signatures or magic bytes.&lt;/li&gt;
&lt;li&gt;Parsing documents with appropriate libraries.&lt;/li&gt;
&lt;li&gt;Fully decoding uploaded images.&lt;/li&gt;
&lt;li&gt;Rejecting malformed or unexpected content.&lt;/li&gt;
&lt;li&gt;Running malware scanning where appropriate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not assume that a valid signature guarantees a safe file. File parsers can contain vulnerabilities, and malicious files may have valid headers.&lt;/p&gt;
&lt;h4 id=&quot;generate-server-controlled-filenames&quot;&gt;Generate server-controlled filenames&lt;/h4&gt;
&lt;p&gt;Never rely on a user-provided filename as the storage path.&lt;/p&gt;
&lt;p&gt;A filename can contain unexpected characters, path separators, or misleading extensions. Generating a UUID or another server-controlled identifier reduces path traversal and collision risks.&lt;/p&gt;
&lt;p&gt;Keep the original filename separately as metadata if the application needs to display it to the user.&lt;/p&gt;
&lt;h4 id=&quot;restrict-access-to-uploaded-files&quot;&gt;Restrict access to uploaded files&lt;/h4&gt;
&lt;p&gt;Uploaded documents should not automatically become public.&lt;/p&gt;
&lt;p&gt;For private documents, verify that the authenticated user is authorized to access the requested file. Store ownership information in a database and enforce permissions on every download request.&lt;/p&gt;
&lt;p&gt;Avoid exposing internal storage paths or allowing users to guess file identifiers and retrieve other users' content.&lt;/p&gt;
&lt;h4 id=&quot;configure-cors-correctly&quot;&gt;Configure CORS correctly&lt;/h4&gt;
&lt;p&gt;Cross-Origin Resource Sharing determines which browser origins may access an API. In production, configure an explicit list of trusted origins.&lt;/p&gt;
&lt;p&gt;CORS is not authentication and does not prevent direct requests from scripts or other clients. Backend authentication and authorization are still required.&lt;/p&gt;
&lt;h3 id=&quot;8-adding-authentication-and-authorization&quot;&gt;8. Adding Authentication and Authorization&lt;/h3&gt;
&lt;p&gt;A production file upload service should usually require an authenticated user.&lt;/p&gt;
&lt;p&gt;For example, a FastAPI dependency can verify a bearer token before allowing an upload:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(
    credentials = Depends(security)
):
    token = credentials.credentials

    user = verify_access_token(token)

    if user is None:
        raise HTTPException(
            status_code=401,
            detail=&quot;Invalid or expired token.&quot;
        )

    return user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;verify_access_token&lt;/code&gt; represents application-specific token verification and must be implemented using a trusted authentication library and appropriate token-validation rules.&lt;/p&gt;
&lt;p&gt;The dependency can then be added to an endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&quot;/api/secure-upload&quot;)
async def secure_upload(
    file: UploadFile = File(...),
    current_user = Depends(get_current_user),
):
    # Validate and store the file.
    # Associate it with current_user.id.
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a real application, the endpoint must also enforce quotas, validate content, and associate the stored file with the correct account.&lt;/p&gt;
&lt;p&gt;Authentication establishes who the user is. Authorization determines what that user is allowed to do. For example, an employee might be allowed to upload documents to their own workspace but not to another organization's workspace.&lt;/p&gt;
&lt;p&gt;These rules must be enforced by the backend, not merely by hiding buttons in React.&lt;/p&gt;
&lt;h3 id=&quot;9-choosing-the-right-storage-strategy&quot;&gt;9. Choosing the Right Storage Strategy&lt;/h3&gt;
&lt;p&gt;The storage solution should match the application's requirements.&lt;/p&gt;
&lt;h4 id=&quot;local-filesystem-storage&quot;&gt;Local filesystem storage&lt;/h4&gt;
&lt;p&gt;Local storage is convenient for development, prototypes, and certain single-server deployments.&lt;/p&gt;
&lt;p&gt;Advantages include straightforward implementation, low initial complexity, and direct access to files.&lt;/p&gt;
&lt;p&gt;However, local storage becomes difficult to manage when multiple application instances need access to the same files. Containers and ephemeral hosting environments may also lose stored data when instances are replaced.&lt;/p&gt;
&lt;h4 id=&quot;object-storage&quot;&gt;Object storage&lt;/h4&gt;
&lt;p&gt;Object storage is usually a better option for scalable applications. Services such as Amazon S3, Google Cloud Storage, and Azure Blob Storage provide durable storage designed for large collections of files.&lt;/p&gt;
&lt;p&gt;Advantages include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Independent scaling of application servers and file storage.&lt;/li&gt;
&lt;li&gt;Lifecycle policies for archiving and deleting files.&lt;/li&gt;
&lt;li&gt;Integration with access controls and encryption.&lt;/li&gt;
&lt;li&gt;Support for direct or multipart uploads.&lt;/li&gt;
&lt;li&gt;Easier distribution of approved public content through a CDN.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For private files, keep buckets private and provide authorized downloads through short-lived signed URLs or a backend download endpoint.&lt;/p&gt;
&lt;h4 id=&quot;database-metadata&quot;&gt;Database metadata&lt;/h4&gt;
&lt;p&gt;Large binary files are often stored outside a relational database, while file metadata is stored in PostgreSQL or another database.&lt;/p&gt;
&lt;p&gt;A metadata record might contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;id
owner_id
original_filename
storage_key
content_type
file_size
checksum
status
created_at
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This structure allows the application to search files, track ownership, manage retention, and enforce permissions without storing every file's bytes directly in a database table.&lt;/p&gt;
&lt;h3 id=&quot;10-uploading-directly-to-object-storage&quot;&gt;10. Uploading Directly to Object Storage&lt;/h3&gt;
&lt;p&gt;As an application grows, sending every file through the Python backend can increase server bandwidth usage and operational costs.&lt;/p&gt;
&lt;p&gt;A common alternative is a presigned-upload workflow.&lt;/p&gt;
&lt;p&gt;The process generally works as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;React requests permission to upload a file.&lt;/li&gt;
&lt;li&gt;The backend authenticates the user and checks upload permissions.&lt;/li&gt;
&lt;li&gt;The backend creates a short-lived presigned URL for an approved object key.&lt;/li&gt;
&lt;li&gt;React uploads the file directly to object storage.&lt;/li&gt;
&lt;li&gt;The application verifies that the object was uploaded successfully.&lt;/li&gt;
&lt;li&gt;The backend records the final metadata and marks the upload as complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach reduces the amount of file data passing through the application server.&lt;/p&gt;
&lt;p&gt;However, the backend must not blindly trust a client-side completion notification. It should verify the object, its size, and relevant metadata using the storage provider's API.&lt;/p&gt;
&lt;p&gt;Presigned URLs should be short-lived and scoped to a specific operation and object. They should not grant broader access than necessary.&lt;/p&gt;
&lt;h3 id=&quot;11-implementing-upload-progress&quot;&gt;11. Implementing Upload Progress&lt;/h3&gt;
&lt;p&gt;Users benefit from seeing how much of a file has been uploaded, particularly when working with larger documents.&lt;/p&gt;
&lt;p&gt;The basic &lt;code&gt;fetch&lt;/code&gt; API does not offer a widely supported browser interface for tracking upload-body progress in the same way that XMLHttpRequest does.&lt;/p&gt;
&lt;p&gt;One option is to use Axios:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An upload function could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import axios from &quot;axios&quot;;

async function uploadFile(file, token, onProgress) {
  const formData = new FormData();

  formData.append(&quot;file&quot;, file);

  const response = await axios.post(
    &quot;http://localhost:8000/api/upload&quot;,
    formData,
    {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      onUploadProgress(event) {
        if (event.total) {
          const percentage = Math.round(
            (event.loaded / event.total) * 100
          );

          onProgress(percentage);
        }
      },
    }
  );

  return response.data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example assumes the application already has a valid access token and that the API requires bearer authentication. The token should come from the application's established authentication flow.&lt;/p&gt;
&lt;p&gt;React can store the reported percentage in state and render a progress bar.&lt;/p&gt;
&lt;p&gt;Keep in mind that upload progress measures the request body being transmitted. It does not necessarily mean that server-side validation, malware scanning, or storage processing has finished.&lt;/p&gt;
&lt;p&gt;A complete user experience should distinguish between uploading, processing, completed, and failed states.&lt;/p&gt;
&lt;h3 id=&quot;12-handling-errors-and-network-failures&quot;&gt;12. Handling Errors and Network Failures&lt;/h3&gt;
&lt;p&gt;Network requests can fail for many reasons, including interrupted connections, server errors, invalid files, expired authentication, and storage failures.&lt;/p&gt;
&lt;p&gt;A robust upload interface should display clear and actionable messages.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;400 Bad Request:&lt;/strong&gt; The uploaded file violates an application rule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;401 Unauthorized:&lt;/strong&gt; The user must authenticate or renew their session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;403 Forbidden:&lt;/strong&gt; The user does not have permission to perform the operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;413 Content Too Large:&lt;/strong&gt; The request exceeds an upload limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;429 Too Many Requests:&lt;/strong&gt; The user has exceeded a rate limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;500 Internal Server Error:&lt;/strong&gt; An unexpected backend failure occurred.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not display raw stack traces or sensitive infrastructure details to users. Log technical details on the server and return a safe error message to the frontend.&lt;/p&gt;
&lt;p&gt;For retry support, use an explicit retry button or a carefully designed automatic retry policy. Avoid blindly retrying every failed POST request because the original upload may have succeeded even if the response was lost.&lt;/p&gt;
&lt;p&gt;For critical workflows, use upload identifiers, idempotency mechanisms, or a status-check endpoint to avoid creating duplicate files.&lt;/p&gt;
&lt;h3 id=&quot;13-supporting-multiple-file-uploads&quot;&gt;13. Supporting Multiple File Uploads&lt;/h3&gt;
&lt;p&gt;Some applications allow users to upload several documents at once.&lt;/p&gt;
&lt;p&gt;React can support this by enabling the &lt;code&gt;multiple&lt;/code&gt; attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;input
  type=&quot;file&quot;
  multiple
  accept=&quot;.pdf,.png,.jpg,.jpeg&quot;
  onChange={(event) =&amp;gt; {
    const selectedFiles = Array.from(
      event.target.files || []
    );

    console.log(selectedFiles);
  }}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend can accept a list of files using FastAPI's &lt;code&gt;list[UploadFile]&lt;/code&gt; type.&lt;/p&gt;
&lt;p&gt;When implementing multi-file uploads, define limits for the number of files, individual file sizes, and total request size.&lt;/p&gt;
&lt;p&gt;It is also useful to track the status of each file independently. If one upload fails, the interface should indicate which file failed and allow the user to retry that file without unnecessarily uploading everything again.&lt;/p&gt;
&lt;p&gt;For large batches, limit concurrency rather than starting hundreds of simultaneous uploads. This reduces pressure on the browser, network, backend, and storage service.&lt;/p&gt;
&lt;h3 id=&quot;14-database-integration-with-postgresql&quot;&gt;14. Database Integration with PostgreSQL&lt;/h3&gt;
&lt;p&gt;A database becomes valuable when users need a file library, search functionality, ownership tracking, or administrative controls.&lt;/p&gt;
&lt;p&gt;A PostgreSQL table could be created as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE uploaded_files (
    id UUID PRIMARY KEY,
    owner_id UUID NOT NULL,
    original_filename TEXT NOT NULL,
    storage_key TEXT NOT NULL UNIQUE,
    content_type TEXT,
    file_size BIGINT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;owner_id&lt;/code&gt; should reference the application's user table in a complete schema. The database should also enforce appropriate constraints for valid statuses, file sizes, and other business rules.&lt;/p&gt;
&lt;p&gt;A typical upload workflow creates a pending record, stores the file, and updates the record after successful storage and validation.&lt;/p&gt;
&lt;p&gt;In a production application, failures must be handled carefully. For example, if the file is stored successfully but the database update fails, the application may need a cleanup job to identify orphaned objects.&lt;/p&gt;
&lt;p&gt;Similarly, if a database record is created but storage fails, the system should mark the upload as failed or remove the incomplete record according to its retention policy.&lt;/p&gt;
&lt;p&gt;Because database transactions do not automatically include external object storage, reliable systems often use explicit upload states and background reconciliation jobs.&lt;/p&gt;
&lt;h3 id=&quot;15-background-processing-and-file-scanning&quot;&gt;15. Background Processing and File Scanning&lt;/h3&gt;
&lt;p&gt;Not every upload can be processed immediately.&lt;/p&gt;
&lt;p&gt;An application might need to generate image thumbnails, extract PDF text, inspect document contents, or scan files for malware.&lt;/p&gt;
&lt;p&gt;Performing these operations inside the main HTTP request can increase response times and reduce API throughput.&lt;/p&gt;
&lt;p&gt;A more scalable design places uploaded files into a processing workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Receive and validate the initial request.&lt;/li&gt;
&lt;li&gt;Store the file in a private location.&lt;/li&gt;
&lt;li&gt;Record the upload as pending.&lt;/li&gt;
&lt;li&gt;Submit a background job.&lt;/li&gt;
&lt;li&gt;Process or scan the file.&lt;/li&gt;
&lt;li&gt;Update the status to approved, rejected, or failed.&lt;/li&gt;
&lt;li&gt;Notify the frontend when the result becomes available.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python tools such as Celery or other task queue systems can help manage background processing.&lt;/p&gt;
&lt;p&gt;The worker should operate with limited permissions and should not execute uploaded files. Files that fail security checks should remain inaccessible to normal users and be handled according to a documented quarantine and deletion policy.&lt;/p&gt;
&lt;h3 id=&quot;16-testing-the-file-upload-system&quot;&gt;16. Testing the File Upload System&lt;/h3&gt;
&lt;p&gt;Testing helps prevent regressions and reveals security weaknesses before deployment.&lt;/p&gt;
&lt;h4 id=&quot;backend-tests&quot;&gt;Backend tests&lt;/h4&gt;
&lt;p&gt;Use a test client to verify that the API accepts valid files and rejects invalid requests.&lt;/p&gt;
&lt;p&gt;Test cases should include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A valid PDF within the configured size limit.&lt;/li&gt;
&lt;li&gt;An unsupported file extension.&lt;/li&gt;
&lt;li&gt;An empty file.&lt;/li&gt;
&lt;li&gt;A file that exceeds the size limit.&lt;/li&gt;
&lt;li&gt;A malformed image or document.&lt;/li&gt;
&lt;li&gt;A missing file field.&lt;/li&gt;
&lt;li&gt;An unauthenticated request.&lt;/li&gt;
&lt;li&gt;An authenticated user attempting to access another user's file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also test cleanup behavior when writing or validation fails.&lt;/p&gt;
&lt;h4 id=&quot;frontend-tests-1&quot;&gt;Frontend tests&lt;/h4&gt;
&lt;p&gt;React tests should verify that the interface displays validation messages, disables the upload button during submission, handles failed responses, and shows the success result.&lt;/p&gt;
&lt;p&gt;Test network interruptions and expired authentication as well as ordinary success scenarios.&lt;/p&gt;
&lt;h4 id=&quot;integration-tests-1&quot;&gt;Integration tests&lt;/h4&gt;
&lt;p&gt;Integration tests confirm that the frontend and backend agree on request field names, response structures, allowed file types, and error formats.&lt;/p&gt;
&lt;p&gt;A file upload system may work correctly in isolation while failing in production because of CORS configuration, reverse-proxy limits, storage permissions, or deployment differences.&lt;/p&gt;
&lt;h3 id=&quot;17-performance-and-scalability-best-practices&quot;&gt;17. Performance and Scalability Best Practices&lt;/h3&gt;
&lt;p&gt;A reliable upload system should be designed around expected file sizes, traffic patterns, and operational requirements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stream data where appropriate.&lt;/strong&gt; Avoid unnecessarily loading large files entirely into application memory. Chunked reading and direct-to-object-storage uploads can reduce resource pressure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use upload limits at multiple layers.&lt;/strong&gt; Application checks should be combined with gateway or reverse-proxy limits to protect the service from oversized requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apply rate limiting and quotas.&lt;/strong&gt; Prevent a single user or automated client from consuming excessive bandwidth or storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep storage independent.&lt;/strong&gt; For distributed deployments, use shared durable storage rather than depending on a single application instance's local filesystem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use background workers.&lt;/strong&gt; Move expensive scanning, conversion, and extraction tasks out of the main request-response path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitor resource consumption.&lt;/strong&gt; Track upload success rates, file sizes, processing latency, storage costs, and failed jobs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plan for cleanup.&lt;/strong&gt; Define retention periods for temporary uploads, abandoned multipart sessions, rejected files, and orphaned objects.&lt;/p&gt;
&lt;p&gt;These practices help maintain predictable performance as usage grows.&lt;/p&gt;
&lt;h3 id=&quot;18-deploying-the-application-to-production&quot;&gt;18. Deploying the Application to Production&lt;/h3&gt;
&lt;p&gt;Before deploying the system, review the entire upload lifecycle.&lt;/p&gt;
&lt;p&gt;Configure environment-specific settings rather than hardcoding storage credentials, secret keys, or production URLs in the source code.&lt;/p&gt;
&lt;p&gt;Use HTTPS to protect data in transit. Keep private files outside publicly accessible directories and ensure that download endpoints enforce authorization.&lt;/p&gt;
&lt;p&gt;Configure reverse-proxy body-size limits consistently with the application's intended upload policy. Verify that the hosting platform, application server, and storage service all support the expected file sizes.&lt;/p&gt;
&lt;p&gt;Add structured logging and monitoring while avoiding the logging of authentication tokens, sensitive file contents, or unnecessary personal information.&lt;/p&gt;
&lt;p&gt;A production deployment should also include dependency updates, backups or appropriate storage durability controls, database migrations, and a recovery plan for failed processing jobs.&lt;/p&gt;
&lt;p&gt;Finally, test the application under realistic conditions. A successful upload on a developer's machine does not prove that the system will work correctly with multiple concurrent users or interrupted network connections.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-6&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a file upload system with React and Python involves much more than selecting a file and sending an HTTP request. A dependable implementation needs a well-designed API, clear frontend feedback, strict server-side validation, secure storage, appropriate access controls, and reliable error handling.&lt;/p&gt;
&lt;p&gt;React makes it possible to create an intuitive interface for selecting files, reporting progress, and displaying upload results. FastAPI provides a practical foundation for receiving and validating files, while PostgreSQL and object storage can support ownership, metadata management, and long-term scalability.&lt;/p&gt;
&lt;p&gt;For a small application, a local-storage implementation can be a useful starting point. As the application grows, private object storage, presigned uploads, background processing, database-backed metadata, and stronger security controls can provide a more scalable architecture.&lt;/p&gt;
&lt;p&gt;The most important principle is to treat every uploaded file as untrusted input. Validate it on the backend, enforce permissions consistently, limit resource consumption, and design for failure from the beginning.&lt;/p&gt;
&lt;p&gt;With these practices in place, developers can build file upload systems that are maintainable, secure, and ready to evolve alongside modern web applications.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31930/building-a-file-upload-system-with-react-and-python</guid>
<pubDate>Wed, 21 Oct 2026 06:50:08 +0000</pubDate>
</item>
<item>
<title>AWS PyTorch: Build, Train, and Deploy Deep Learning Models in the Cloud</title>
<link>https://coderlegion.com/31929/aws-pytorch-build-train-and-deploy-deep-learning-models-in-the-cloud</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;AWS PyTorch: Build, Train, and Deploy Deep Learning Models in the Cloud&quot;&lt;br&gt;
published: true&lt;br&gt;
description: &quot;Learn how to build a PyTorch image classifier, store model artifacts in Amazon S3, and move toward managed machine learning training and deployment with Amazon SageMaker AI.&quot;&lt;/p&gt;
&lt;h3 id=&quot;tags-aws-pytorch-machinelearning-python&quot;&gt;tags: aws, pytorch, machinelearning, python&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/neldd08v6n3oeqcj5r1g.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;PyTorch makes it easier to experiment with neural networks, computer vision, natural language processing, and deep learning.&lt;/p&gt;
&lt;p&gt;But building a model is only one part of a machine learning project.&lt;/p&gt;
&lt;p&gt;When it's time to move from experimentation to production, you need reliable compute, data storage, repeatable training, model artifacts, deployment, monitoring, and cost controls.&lt;/p&gt;
&lt;p&gt;This is where AWS can help.&lt;/p&gt;
&lt;p&gt;By combining &lt;strong&gt;PyTorch, Amazon SageMaker AI, Amazon S3, and Amazon CloudWatch&lt;/strong&gt;, developers can build machine learning workflows that move from local experiments toward scalable cloud deployments.&lt;/p&gt;
&lt;p&gt;In this article, we'll build a simple image classifier and explore how to extend the project into an AWS-based machine learning pipeline.&lt;/p&gt;
&lt;h3 id=&quot;1-what-is-pytorch-on-aws-&quot;&gt;1. What Is PyTorch on AWS?&lt;/h3&gt;
&lt;p&gt;PyTorch is an open-source deep learning framework that provides tensor operations, automatic differentiation, GPU acceleration, and tools for creating neural networks.&lt;/p&gt;
&lt;p&gt;AWS provides several services that can support PyTorch workloads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Amazon SageMaker AI:&lt;/strong&gt; Run managed training jobs and deploy machine learning models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon S3:&lt;/strong&gt; Store datasets, model artifacts, and training outputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon EC2:&lt;/strong&gt; Run PyTorch on virtual machines, including suitable GPU instances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS Deep Learning Containers:&lt;/strong&gt; Use supported, preconfigured machine learning environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon CloudWatch:&lt;/strong&gt; Monitor supported metrics, logs, and application behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS IAM:&lt;/strong&gt; Control access to training data, infrastructure, and model artifacts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need all these services for every project. Start with what your workload requires and add complexity when it provides a clear benefit.&lt;/p&gt;
&lt;h3 id=&quot;2-understand-the-architecture&quot;&gt;2. Understand the Architecture&lt;/h3&gt;
&lt;p&gt;A typical workflow looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;        Training Dataset
               |
               v
         Amazon S3
               |
               v
      Amazon SageMaker AI
          Training Job
               |
               v
         PyTorch Model
               |
               v
       Model Artifacts in S3
               |
               v
       Deployment / Inference
               |
               v
       Application Predictions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Amazon S3 provides durable storage for datasets and artifacts. SageMaker AI can run managed training jobs, while a suitable inference service can expose the trained model to applications.&lt;/p&gt;
&lt;p&gt;CloudWatch and related AWS monitoring capabilities help you investigate failures, latency, and resource usage.&lt;/p&gt;
&lt;p&gt;For small experiments, you can train locally first. For larger workloads, managed training can reduce infrastructure-management overhead.&lt;/p&gt;
&lt;h3 id=&quot;3-hands-on-project-build-a-digit-classifier&quot;&gt;3. Hands-On Project: Build a Digit Classifier&lt;/h3&gt;
&lt;p&gt;We'll train a small neural network on the MNIST handwritten-digit dataset.&lt;/p&gt;
&lt;p&gt;Our model will learn to classify grayscale images into one of ten categories, representing the digits from 0 to 9.&lt;/p&gt;
&lt;h4 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h4&gt;
&lt;p&gt;You'll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.&lt;/li&gt;
&lt;li&gt;Basic Python knowledge.&lt;/li&gt;
&lt;li&gt;An internet connection to download the dataset.&lt;/li&gt;
&lt;li&gt;An AWS account only when you move to the cloud steps.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first part runs locally and does not require AWS resources.&lt;/p&gt;
&lt;h4 id=&quot;step-1-create-a-project-environment&quot;&gt;Step 1: Create a project environment&lt;/h4&gt;
&lt;p&gt;Create a project directory and enter it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir aws-pytorch-demo
cd aws-pytorch-demo
python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Activate the virtual environment on Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;.venv\Scripts\Activate.ps1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Linux or macOS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install torch torchvision
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For GPU training, install a PyTorch build compatible with your GPU and CUDA configuration. Follow the official installation instructions: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pytorch.org/get-started/locally/&quot;&gt;https://pytorch.org/get-started/locally/&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;step-2-create-the-training-script&quot;&gt;Step 2: Create the training script&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;train.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

device = torch.device(
    &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;
)

print(f&quot;Training device: {device}&quot;)

transform = transforms.ToTensor()

train_data = datasets.MNIST(
    root=&quot;data&quot;,
    train=True,
    download=True,
    transform=transform,
)

test_data = datasets.MNIST(
    root=&quot;data&quot;,
    train=False,
    download=True,
    transform=transform,
)

train_loader = DataLoader(
    train_data,
    batch_size=64,
    shuffle=True,
)

test_loader = DataLoader(
    test_data,
    batch_size=256,
    shuffle=False,
)


class DigitClassifier(nn.Module):
    def __init__(self):
        super().__init__()

        self.network = nn.Sequential(
            nn.Flatten(),
            nn.Linear(28 * 28, 128),
            nn.ReLU(),
            nn.Linear(128, 10),
        )

    def forward(self, x):
        return self.network(x)


model = DigitClassifier().to(device)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001,
)

# Training loop
for epoch in range(3):
    model.train()
    total_loss = 0.0

    for images, labels in train_loader:
        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)
        loss = criterion(outputs, labels)

        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    average_loss = total_loss / len(train_loader)

    print(
        f&quot;Epoch {epoch + 1}/3, &quot;
        f&quot;Loss: {average_loss:.4f}&quot;
    )

# Evaluation
model.eval()
correct = 0
total = 0

with torch.no_grad():
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)
        predictions = outputs.argmax(dim=1)

        correct += (predictions == labels).sum().item()
        total += labels.size(0)

accuracy = 100 * correct / total
print(f&quot;Test accuracy: {accuracy:.2f}%&quot;)

# Save model parameters
torch.save(
    model.state_dict(),
    &quot;digit_classifier.pth&quot;,
)

print(&quot;Saved digit_classifier.pth&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-train-the-model&quot;&gt;Step 3: Train the model&lt;/h4&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python train.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script downloads MNIST, trains the neural network, evaluates it against the test dataset, and saves the learned parameters.&lt;/p&gt;
&lt;p&gt;Your output should include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Training device: cpu
Epoch 1/3, Loss: ...
Epoch 2/3, Loss: ...
Epoch 3/3, Loss: ...
Test accuracy: ...
Saved digit_classifier.pth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The loss and accuracy values depend on the training run. If a compatible GPU is available, the script can use it automatically.&lt;/p&gt;
&lt;h4 id=&quot;step-4-make-a-prediction&quot;&gt;Step 4: Make a prediction&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;predict.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch
from torch import nn
from torchvision import datasets, transforms


class DigitClassifier(nn.Module):
    def __init__(self):
        super().__init__()

        self.network = nn.Sequential(
            nn.Flatten(),
            nn.Linear(28 * 28, 128),
            nn.ReLU(),
            nn.Linear(128, 10),
        )

    def forward(self, x):
        return self.network(x)


device = torch.device(
    &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;
)

model = DigitClassifier().to(device)

state = torch.load(
    &quot;digit_classifier.pth&quot;,
    map_location=device,
    weights_only=True,
)

model.load_state_dict(state)
model.eval()

dataset = datasets.MNIST(
    root=&quot;data&quot;,
    train=False,
    download=True,
    transform=transforms.ToTensor(),
)

image, actual_label = dataset[0]

with torch.no_grad():
    logits = model(image.unsqueeze(0).to(device))
    predicted_label = logits.argmax(dim=1).item()

print(f&quot;Predicted digit: {predicted_label}&quot;)
print(f&quot;Actual digit: {actual_label}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the prediction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python predict.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Predicted digit: 7
Actual digit: 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These values are illustrative. The actual result depends on your trained model.&lt;/p&gt;
&lt;p&gt;You now have a basic end-to-end deep learning workflow: train, evaluate, save, reload, and predict.&lt;/p&gt;
&lt;h3 id=&quot;4-move-the-project-to-aws&quot;&gt;4. Move the Project to AWS&lt;/h3&gt;
&lt;p&gt;Once the model works locally, you can move the workflow to AWS.&lt;/p&gt;
&lt;h4 id=&quot;step-1-create-an-s3-bucket&quot;&gt;Step 1: Create an S3 bucket&lt;/h4&gt;
&lt;p&gt;Choose a globally unique bucket name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws s3 mb s3://YOUR-UNIQUE-PYTORCH-BUCKET \
  --region us-east-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the placeholder with your actual bucket name.&lt;/p&gt;
&lt;p&gt;Upload the training script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws s3 cp train.py \
  s3://YOUR-UNIQUE-PYTORCH-BUCKET/code/train.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this example, the script downloads MNIST directly. A production pipeline should manage dataset preparation, storage, and versioning more explicitly.&lt;/p&gt;
&lt;h4 id=&quot;step-2-run-training-with-sagemaker-ai&quot;&gt;Step 2: Run training with SageMaker AI&lt;/h4&gt;
&lt;p&gt;Amazon SageMaker AI supports PyTorch training jobs using supported framework environments and training configurations.&lt;/p&gt;
&lt;p&gt;At a high level, you need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configure an execution role with appropriate permissions.&lt;/li&gt;
&lt;li&gt;Prepare your training script and dependencies.&lt;/li&gt;
&lt;li&gt;Select a supported PyTorch environment.&lt;/li&gt;
&lt;li&gt;Configure the training input and output locations.&lt;/li&gt;
&lt;li&gt;Choose an appropriate instance type.&lt;/li&gt;
&lt;li&gt;Set runtime limits and launch the job.&lt;/li&gt;
&lt;li&gt;Review the training logs and saved artifacts.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Use the current official guide for the SDK version you install:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/sagemaker/latest/dg/pytorch.html&quot;&gt;https://docs.aws.amazon.com/sagemaker/latest/dg/pytorch.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;SageMaker SDK interfaces evolve, so use examples matching your installed SDK version rather than mixing older estimator APIs with newer SDK workflows.&lt;/p&gt;
&lt;h4 id=&quot;step-3-store-model-artifacts-in-s3&quot;&gt;Step 3: Store model artifacts in S3&lt;/h4&gt;
&lt;p&gt;For a locally trained model, you can upload the saved parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws s3 cp digit_classifier.pth \
  s3://YOUR-UNIQUE-PYTORCH-BUCKET/models/digit_classifier.pth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a managed training job, configure the output location so SageMaker AI saves the model artifacts automatically.&lt;/p&gt;
&lt;p&gt;Keep a record of the dataset version, source-code version, training parameters, framework version, and evaluation metrics. This makes it easier to reproduce and audit experiments.&lt;/p&gt;
&lt;h3 id=&quot;5-deploy-the-model-for-inference&quot;&gt;5. Deploy the Model for Inference&lt;/h3&gt;
&lt;p&gt;Training and inference are separate stages.&lt;/p&gt;
&lt;p&gt;For an application to request predictions, you need a serving interface that loads the model, validates input, executes inference, and returns a response.&lt;/p&gt;
&lt;p&gt;With Amazon SageMaker AI, you can deploy supported PyTorch models to a managed endpoint. The required model packaging and inference code depend on the selected deployment approach.&lt;/p&gt;
&lt;p&gt;A typical inference flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load the approved model artifact.&lt;/li&gt;
&lt;li&gt;Convert the input into the tensor format expected by the model.&lt;/li&gt;
&lt;li&gt;Run the model in evaluation mode.&lt;/li&gt;
&lt;li&gt;Convert the output into a response your application can consume.&lt;/li&gt;
&lt;li&gt;Log relevant errors and operational metrics.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the MNIST model, input should represent a grayscale image with dimensions of 28 × 28 pixels. Validate the request before running inference.&lt;/p&gt;
&lt;p&gt;For workloads that don't need immediate responses, consider batch inference instead of keeping an online endpoint available continuously.&lt;/p&gt;
&lt;h3 id=&quot;6-production-best-practices&quot;&gt;6. Production Best Practices&lt;/h3&gt;
&lt;p&gt;A model that works locally isn't automatically production-ready.&lt;/p&gt;
&lt;h4 id=&quot;cost-optimization&quot;&gt;Cost optimization&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Start with CPU training for small models when practical.&lt;/li&gt;
&lt;li&gt;Use GPU instances when benchmarks show a meaningful benefit.&lt;/li&gt;
&lt;li&gt;Set job time limits.&lt;/li&gt;
&lt;li&gt;Stop endpoints and compute resources when they are no longer required.&lt;/li&gt;
&lt;li&gt;Evaluate batch inference for offline workloads.&lt;/li&gt;
&lt;li&gt;Include storage, logging, data transfer, and supporting services in cost estimates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;security&quot;&gt;Security&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Apply least-privilege IAM policies.&lt;/li&gt;
&lt;li&gt;Keep S3 buckets private.&lt;/li&gt;
&lt;li&gt;Encrypt sensitive datasets and artifacts.&lt;/li&gt;
&lt;li&gt;Avoid embedding credentials in source code.&lt;/li&gt;
&lt;li&gt;Restrict access to training data and deployed models.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;reproducibility&quot;&gt;Reproducibility&lt;/h4&gt;
&lt;p&gt;Track dataset versions, dependencies, training parameters, random seeds, and model artifacts. Store code in version control and preserve evaluation results for each model version.&lt;/p&gt;
&lt;h4 id=&quot;monitoring&quot;&gt;Monitoring&lt;/h4&gt;
&lt;p&gt;Monitor training failures, compute utilization, inference latency, and application errors using supported AWS monitoring capabilities.&lt;/p&gt;
&lt;p&gt;Also evaluate model quality over time. A service can remain operational while its predictions become less accurate because real-world data has changed.&lt;/p&gt;
&lt;h4 id=&quot;model-evaluation&quot;&gt;Model evaluation&lt;/h4&gt;
&lt;p&gt;Don't rely on training loss alone.&lt;/p&gt;
&lt;p&gt;For classification tasks, consider precision, recall, F1 score, confusion matrices, and performance on representative held-out data. Select metrics based on the real business problem.&lt;/p&gt;
&lt;h3 id=&quot;7-where-can-you-use-pytorch-on-aws-&quot;&gt;7. Where Can You Use PyTorch on AWS?&lt;/h3&gt;
&lt;p&gt;Common applications include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Computer vision:&lt;/strong&gt; Image classification and defect detection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Natural language processing:&lt;/strong&gt; Text classification and domain-specific model fine-tuning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommendation systems:&lt;/strong&gt; Ranking and personalization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forecasting:&lt;/strong&gt; Demand prediction and time-series analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generative AI:&lt;/strong&gt; Training or fine-tuning compatible neural network models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For large-scale distributed training, additional planning is needed for GPU memory, network communication, checkpointing, and data loading.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;PyTorch provides the flexibility to build deep learning models, while AWS offers several ways to run training workloads, store model artifacts, and deploy predictions.&lt;/p&gt;
&lt;p&gt;Start with a small experiment, validate the results, and then introduce cloud infrastructure where it provides measurable value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The real goal is not just to train a model in the cloud. It's to create a secure, reproducible, observable machine learning workflow that can grow with your application.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;official-resources&quot;&gt;Official Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/sagemaker/latest/dg/pytorch.html&quot;&gt;PyTorch on Amazon SageMaker AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://aws.amazon.com/blogs/machine-learning/&quot;&gt;AWS machine learning blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/s3/&quot;&gt;Amazon S3 documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/cloudwatch/&quot;&gt;Amazon CloudWatch documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pytorch.org/get-started/locally/&quot;&gt;Install PyTorch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;aws-pytorch-machinelearning-deeplearning-amazonsagemaker&quot;&gt;AWS #PyTorch #MachineLearning #DeepLearning #AmazonSageMaker&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31929/aws-pytorch-build-train-and-deploy-deep-learning-models-in-the-cloud</guid>
<pubDate>Wed, 21 Oct 2026 06:46:29 +0000</pubDate>
</item>
<item>
<title>Building a Small RAG Index from Documentation Pages: Map First, Then Scrape</title>
<link>https://coderlegion.com/31890/building-a-small-rag-index-from-documentation-pages-map-first-then-scrape</link>
<description>&lt;p&gt;When you want an LLM to answer questions about a product's docs, the hard part is rarely the vector store. It is getting clean, current text out of the pages in the first place. Raw HTML is full of navigation, cookie banners and script tags that waste tokens and confuse retrieval. Here is the simple three-step workflow I use.&lt;/p&gt;
&lt;h3 id=&quot;1-map-the-section-you-care-about&quot;&gt;1. Map the section you care about&lt;/h3&gt;
&lt;p&gt;Before fetching anything, list the URLs under the docs path (for example docs.example.com/guides). A sitemap or a &quot;map&quot; endpoint gives you the full list in one call, so you can drop changelogs, blog posts or old version folders before you spend any requests on them.&lt;/p&gt;
&lt;h3 id=&quot;2-scrape-only-the-pages-you-kept-as-markdown&quot;&gt;2. Scrape only the pages you kept, as Markdown&lt;/h3&gt;
&lt;p&gt;Markdown keeps headings, lists and code blocks but drops the page chrome. It is usually several times smaller than the HTML and chunks naturally on headings, which makes retrieval more precise. Save each page with its source URL so answers can cite where they came from.&lt;/p&gt;
&lt;h3 id=&quot;3-chunk-on-headings-and-embed&quot;&gt;3. Chunk on headings and embed&lt;/h3&gt;
&lt;p&gt;Split each Markdown file on H2/H3 headings, keep chunks around 300 to 800 tokens, and store the URL plus heading as metadata. Re-run the map step on a schedule and only re-scrape pages that are new or changed.&lt;/p&gt;
&lt;h3 id=&quot;tools&quot;&gt;Tools&lt;/h3&gt;
&lt;p&gt;You can do steps 1 and 2 with your own fetcher plus an HTML-to-Markdown converter. If you would rather not maintain that, a hosted web data API does the same job: for example &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.xcrawl.ai/&quot;&gt;XCrawl&lt;/a&gt; has map, scrape, search and crawl endpoints that return Markdown or JSON, a Python/Node SDK, and an MCP server (npm package xcrawl-mcp) so Claude or Cursor can call it directly. New accounts start with 1,000 free credits, enough to index a mid-sized docs site and test the pipeline.&lt;/p&gt;
&lt;h3 id=&quot;a-few-rules-of-thumb&quot;&gt;A few rules of thumb&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Map before you crawl, so you never fetch pages you will throw away.&lt;/li&gt;
&lt;li&gt;Keep the source URL with every chunk.&lt;/li&gt;
&lt;li&gt;Respect robots.txt and each site's terms, and keep request rates gentle on small sites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the whole pipeline: map, scrape to Markdown, chunk on headings. It is boring, which is exactly what you want from data plumbing.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31890/building-a-small-rag-index-from-documentation-pages-map-first-then-scrape</guid>
<pubDate>Tue, 20 Oct 2026 21:15:41 +0000</pubDate>
</item>
<item>
<title>I’m Still Learning AI Coding: The Rules and Workflows Helping Me Find My Way</title>
<link>https://coderlegion.com/31885/im-still-learning-ai-coding-the-rules-and-workflows-helping-me-find-my-way</link>
<description>&lt;p&gt;&lt;em&gt;What building EthoPipe is teaching me about AI tools, project rules, and leaving room to experiment&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Update: I've linked the companion guide :) &lt;/p&gt;
&lt;p&gt;After my post &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31633/how-adobe-firefly-and-copilot-taught-me-to-prompt-for-code-part-1&quot;&gt;How Adobe Firefly and Copilot Taught Me to Prompt for Code (Part 1)&lt;/a&gt;, a reader asked whether smaller coding prompts worked better than one detailed prompt. I answered yes, but the question stayed with me. What did “smaller tasks” actually look like in my project—and where did rules and workflows fit?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I am not an expert in AI-assisted development. I am learning by building something I care about.&lt;/strong&gt; These are the files and habits helping me find my way. You might borrow one part, change another, and ignore the rest.&lt;/p&gt;
&lt;p&gt;I used to be strongly against AI. I refused to use it. Now I am writing about how I work with it—not because I suddenly have everything figured out, but because I changed my mind and started experimenting.&lt;/p&gt;
&lt;p&gt;My project is EthoPipe, an open-science Python project for canine ethology. At one point, I restarted because I was getting lost among ideas, generated code, instructions, and unfinished tasks. I wanted the tools to help me follow the project, not produce more work I could no longer understand.&lt;/p&gt;
&lt;p&gt;My Firefly experiments gave me a useful starting point. A short image prompt could work because the reference photo and settings carried part of the instruction. With coding, I began asking the same practical question: what does the agent need to know before I ask it to change something?&lt;/p&gt;
&lt;h3 id=&quot;1-decide-what-i-want-before-asking-the-agent-to-build-it&quot;&gt;1. Decide what I want before asking the agent to build it&lt;/h3&gt;
&lt;p&gt;I usually brainstorm in the Gemini app, then move to a coding agent when the direction is clearer. Sometimes I use Copilot for another perspective or when I feel stuck. An IDE agent can discuss ideas too; separating the conversations simply helps me avoid turning every possibility into a feature.&lt;/p&gt;
&lt;p&gt;An idea can stay an idea. Before moving to code, I ask: &lt;strong&gt;What am I changing? Which files belong to the task? What should stay untouched? How will I know it worked?&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;what-changed-between-july-and-october&quot;&gt;What changed between July and October&lt;/h4&gt;
&lt;p&gt;My July files already contain plans, audits, and walkthroughs. They helped me notice problems such as a filename mismatch and checks that were configured but not working. I was learning to pay attention to the work around the code, not just the code itself.&lt;/p&gt;
&lt;p&gt;By October, I had an idea board, more specific project rules, scripts for recording stopping points, and a handoff process. I did not design that system in advance. I kept adding or adapting a file when the same confusion came back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;These files make the next session easier.&lt;/strong&gt; Some guide the agent, some automate repetitive work, and some remind me where I left off.&lt;/p&gt;
&lt;h3 id=&quot;2-my-rules-guide-the-agent-not-my-imagination&quot;&gt;2. My rules guide the agent—not my imagination&lt;/h3&gt;
&lt;p&gt;When I moved my instructions from Gemini Code Assist to Antigravity, some did not work the way I expected. I had to redo parts of the setup. That was frustrating, but it also made me pay more attention to where the files belonged and how the tool used them.&lt;/p&gt;
&lt;p&gt;The distinction that helps me is simple: &lt;strong&gt;rules&lt;/strong&gt; describe the project’s boundaries; &lt;strong&gt;workflows&lt;/strong&gt; describe how I approach a recurring task; &lt;strong&gt;scripts&lt;/strong&gt; handle repetitive checks or record-keeping. Each has a different job.&lt;/p&gt;
&lt;p&gt;The packaging changes too. Antigravity now documents &lt;a rel=&quot;nofollow&quot; href=&quot;https://antigravity.google/docs/skills&quot;&gt;skills&lt;/a&gt; for reusable task procedures and &lt;a rel=&quot;nofollow&quot; href=&quot;https://antigravity.google/docs/rules&quot;&gt;rules&lt;/a&gt; for persistent instructions. I still call the overall process my workflow. If you borrow my files, check the current documentation for your tool rather than assuming the old folder structure will work unchanged.&lt;/p&gt;
&lt;p&gt;I still review what the agent changes. “Do not touch this” is a useful instruction, not a reason to stop paying attention.&lt;/p&gt;
&lt;h4 id=&quot;one-example-keeping-the-project-small-enough-for-me&quot;&gt;One example: keeping the project small enough for me&lt;/h4&gt;
&lt;p&gt;I do not want the agent adding a setup I cannot understand or maintain. My solo-maintainer rules limit dependencies, extra languages, and unnecessary infrastructure. Those boundaries fit EthoPipe, and I can revise them when the project needs something different.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A few lines from my &lt;code&gt;solo-maintainer.md&lt;/code&gt; file:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;- Direct runtime dependencies in pyproject.toml are capped at &amp;lt;= 8 packages.
- Always prefer Python's standard library
  (pathlib, tomllib, dataclasses, argparse, typing, sqlite3)
  over adding third-party packages.
- Keep EthoPipe as a single modular Python service
  (FastAPI + Supabase).
- Individual files must remain under 600 lines.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your version might need a frontend framework, another database, or more dependencies. The useful part is deciding what fits your project and writing it down, rather than explaining it again in every conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The constraints guide the AI’s actions, not my imagination.&lt;/strong&gt; I can explore another direction without authorizing new packages, rewritten files, or unrequested features. Not every possibility needs to become code.&lt;/p&gt;
&lt;h3 id=&quot;3-give-a-smaller-task-a-clear-purpose&quot;&gt;3. Give a smaller task a clear purpose&lt;/h3&gt;
&lt;p&gt;My idea-to-code file follows &lt;strong&gt;triage → specification → test first → minimal implementation → QA and journal.&lt;/strong&gt; That may sound formal, but it helps me pause long enough to decide what I need before files start changing.&lt;/p&gt;
&lt;p&gt;Instead of “improve my project,” I ask for one change and name the files involved—including tests. If the agent finds a larger issue, I want it to tell me rather than quietly turn the task into something else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here is an example of how I would describe a small task:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Objective:
Check the subject_id length boundary.

Allowed files:
src/pipeline/models.py
tests/test_models.py

Non-goals:
No dependency, CI, or unrelated refactoring changes.

Acceptance:
255 characters accepted; 256 rejected.

Process:
Inspect existing tests. Add a regression test only if missing.
Make the smallest necessary change. Run targeted and full checks.

Stop:
Ask if requirements conflict or the boundary is undocumented.

Closeout:
Changed files, commands and results, blockers, next action.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prompt is not especially short; it is specific. In this example, the limit and tests already exist in my snapshot, so inspection comes before generating anything new.&lt;/p&gt;
&lt;p&gt;That is why my answer is not “always write short prompts.” A longer discussion can establish the direction; smaller tasks carry it forward. The rules provide project context, and the task says what I want now.&lt;/p&gt;
&lt;h3 id=&quot;4-leave-myself-a-way-to-come-back&quot;&gt;4. Leave myself a way to come back&lt;/h3&gt;
&lt;p&gt;I often open a new conversation for a particular task. It keeps unrelated ideas out of the way, but I do not want to explain the whole project again. My state and handoff files help bridge those sessions.&lt;/p&gt;
&lt;p&gt;The state file records what I was doing, where I stopped, and what comes next. A handoff adds relevant project changes and checks when requested. Neither me nor the agent has to remember every detail of the previous conversation.&lt;/p&gt;
&lt;h4 id=&quot;the-kind-of-note-that-helps-me-resume&quot;&gt;The kind of note that helps me resume&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;From my October 12 &lt;code&gt;PROJECT_STATE.md&lt;/code&gt; checkpoint:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Where We Stopped:
Completed project-state skill, workflow, CLI engine
(scripts/manage_project_state.py), and state.ps1 helper
with docs/states/ historical archiving.

Quick-Resume Action:
Proceed to localized storage adapter design for Kimball
Star Schema persistence or review project state via
.\scripts\state.ps1.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The note is ordinary—and that is why it helps. I can return to it and know where to start. One checkpoint recorded the state tooling in progress; the next gave me a new task. The files keep that thread intact.&lt;/p&gt;
&lt;h3 id=&quot;5-ask-for-help-read-the-docs-and-keep-adjusting&quot;&gt;5. Ask for help, read the docs, and keep adjusting&lt;/h3&gt;
&lt;p&gt;When I do not know how to write a rule or workflow, I ask Gemini to help. I explain what I want, work through a draft, compare it with the documentation, and try it. If I cannot describe the goal, that is a sign I need to explore the idea a little longer.&lt;/p&gt;
&lt;p&gt;I work with the agent, review changes, and ask for a walkthrough when we finish. I want to understand what happened and what comes next. The goal is not to pretend I could have written every line unaided; it is to learn enough to keep working on my project.&lt;/p&gt;
&lt;p&gt;Google’s learning resources—including GEAR and courses available through my student benefits and learning credits—helped me understand the tools. The documentation also helped explain why instructions from one environment needed adapting in another.&lt;/p&gt;
&lt;h3 id=&quot;6-coding-experience-helps-but-the-tool-still-needs-learning&quot;&gt;6. Coding experience helps—but the tool still needs learning&lt;/h3&gt;
&lt;p&gt;I come from outside the usual developer path. I cannot speak for someone with twenty years of coding experience, but I can offer a beginner’s perspective: I am still asking questions about how these tools work.&lt;/p&gt;
&lt;p&gt;Coding experience and familiarity with a coding agent are related, but not identical. Recognizing a bad abstraction or debugging a problem is valuable. Learning how a particular agent reads instructions, carries context, asks for approval, and handles a task is another skill. Experience gives you something to build on; it does not remove the need to learn the tool.&lt;/p&gt;
&lt;p&gt;I wonder whether the familiar setting makes that easy to overlook. It is still an editor, a terminal, and a repository, but some of the interaction now happens through conversation. Reading a tutorial or asking how a feature works does not diminish anyone’s experience.&lt;/p&gt;
&lt;p&gt;I would start with an ordinary task: explain an unfamiliar function, suggest a test, or make a change I can follow. Then I can inspect the result and adjust my instructions. That gives me room to learn without expecting the first prompt to solve everything.&lt;/p&gt;
&lt;p&gt;Having once refused to use AI, I am reluctant to insist that anyone must embrace it. But curiosity is enough reason to try. A question or mistake does not have to become a verdict on you—or on the technology.&lt;/p&gt;
&lt;h4 id=&quot;i-do-not-think-you-need-the-most-expensive-tool-to-begin&quot;&gt;I do not think you need the most expensive tool to begin&lt;/h4&gt;
&lt;p&gt;Some articles leave me with the impression that useful AI-assisted coding requires the newest model or most expensive subscription. My experience makes me question that. I think an accessible tool can produce decent work when it fits the project and you take time to learn it.&lt;/p&gt;
&lt;p&gt;I started with the access I had. Gemini became familiar, and search fits the way I research. An upgrade might help with a specific limitation, but I do not want a price tag to make someone feel they cannot begin. Learning to describe the task has mattered more to me than chasing the model everyone is discussing that week.&lt;/p&gt;
&lt;h3 id=&quot;7-experimentation-is-a-reason-to-build-not-a-consolation-prize&quot;&gt;7. Experimentation is a reason to build—not a consolation prize&lt;/h3&gt;
&lt;h4 id=&quot;a-personal-project-can-be-a-place-to-play&quot;&gt;A personal project can be a place to play&lt;/h4&gt;
&lt;p&gt;I do not know whether EthoPipe will become something bigger. It already gives me a reason to learn, make mistakes, ask questions, and try ideas. That is not a consolation prize if it never becomes a finished product. It is a reason to build.&lt;/p&gt;
&lt;p&gt;A personal project does not have to become a company, a polished product, or proof that you are a “real” developer. You can vibe code, run tests, discard an experiment, and start again. &lt;strong&gt;AI can help you be more creative, not just more productive.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I can ask for three approaches without authorizing file changes, choose one, and try it separately. I can ask the agent to explain the result—or discard it if it does not help. Exploring possibilities is different from adding all of them to the main project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The agent needs a clear job; my ideas need room.&lt;/strong&gt; I can change the rules, explore another direction, or leave an experiment unfinished. The boundaries help me follow the work, not postpone it.&lt;/p&gt;
&lt;h4 id=&quot;if-you-want-to-try-this-start-with-one-file&quot;&gt;If you want to try this, start with one file&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Pick a task you keep repeating or a problem that keeps confusing you.&lt;/li&gt;
&lt;li&gt;Write a small rule or procedure in your own words.&lt;/li&gt;
&lt;li&gt;Ask your AI tool to help clarify it, then check how your environment loads it.&lt;/li&gt;
&lt;li&gt;Try it on one manageable task and look at the result.&lt;/li&gt;
&lt;li&gt;Keep what helps, change what does not, and leave yourself a next step.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You do not need my full collection. I am sharing it because concrete examples would have helped me understand what people meant by rules and workflows. Take what solves a problem for you, then adapt the stack, limits, and language.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Smaller tasks help me, but I am not trying to make my ideas smaller.&lt;/strong&gt; I am giving the agent a clearer job while I keep learning and experimenting. The prompt says what to do now. What I imagine next is still up to me.&lt;/p&gt;
&lt;p&gt;What have you tried with AI simply to learn or explore? I would love to hear about the experiments—not just the finished products.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI assistance:&lt;/strong&gt; I used Gemini and Microsoft Copilot to brainstorm, organize, and edit this article. The experience and project files are mine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My files:&lt;/strong&gt; the companion guide, “&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/32111/my-ai-coding-project-files-what-you-can-borrow&quot;&gt;My AI Coding Project Files: What You Can Borrow&lt;/a&gt;,” groups the rules, workflows, scripts, and records behind these examples so you can choose one useful starting point.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31885/im-still-learning-ai-coding-the-rules-and-workflows-helping-me-find-my-way</guid>
<pubDate>Tue, 20 Oct 2026 19:22:32 +0000</pubDate>
</item>
<item>
<title>Hiding Agent Reasoning Costs You Your Debug Trail</title>
<link>https://coderlegion.com/31725/hiding-agent-reasoning-costs-you-your-debug-trail</link>
<description>&lt;p&gt;There's a real push toward agents that reason internally - in vectors, not words - and only emit text when they act. It's faster and cheaper. It also quietly removes the thing most teams rely on to figure out why an agent did something strange. That's a decision worth making on purpose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Idea: Visible Reasoning Is a Logging Choice, Not a Model Feature&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When an agent &quot;thinks out loud,&quot; it generates intermediate text before acting. Those tokens cost money and latency, but they appear in your logs. When something breaks in production, the reasoning trace is usually the only artifact that tells you whether the model misread the input, picked the wrong tool, or got bad data back.&lt;/p&gt;
&lt;p&gt;Latent reasoning - where the model does its intermediate work in internal representations (numeric vectors) rather than generated words - skips that output. You get the action without the narration. You save tokens and wall-clock time, and you lose the human-readable record of how the decision was reached.&lt;/p&gt;
&lt;p&gt;Which steps need a readable trace and which don't. A step that routes a query between two retrieval sources rarely needs explanation. A step that approves a refund, rejects a candidate, or sends a customer-facing email does. Treating reasoning visibility as a per-step configuration - not a global model setting - is the move that holds up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real Example: Tag Steps by Audit Requirement&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Make the trade explicit in your agent config rather than inheriting whatever the framework defaults to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;STEPS = {
 &quot;route_query&quot;: {&quot;explain&quot;: False}, # cheap, low-stakes
 &quot;fetch_policy&quot;: {&quot;explain&quot;: False},
 &quot;decide_refund&quot;: {&quot;explain&quot;: True}, # money moves, must justify
 &quot;draft_reply&quot;: {&quot;explain&quot;: True}, # customer-facing
}

def run(step, ctx):
 cfg = STEPS[step]
 out = model.call(step, ctx, reasoning_output=cfg[&quot;explain&quot;])
 if cfg[&quot;explain&quot;]:
 log.audit(step=step, rationale=out.rationale, action=out.action)
 return out.action
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things fall out of this. First, you can measure the cost of auditability instead of guessing - compare token spend with &lt;code&gt;explain&lt;/code&gt; on versus off across a week of real traffic. Second, when a refund goes wrong, you have a rationale for that exact step and nothing wasted on the routing step nobody will ever review.&lt;/p&gt;
&lt;p&gt;A PM can read that table and ask the right question in planning: &lt;em&gt;which of these steps would we need to defend to a customer, a regulator, or our own support team?&lt;/em&gt; That list, not the model architecture, decides where you spend tokens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Latent reasoning trades a readable decision trail for lower latency and token cost - the speed gain is real, and so is the loss.&lt;/li&gt;
&lt;li&gt;Set reasoning visibility per step, not per system; routing decisions and refund approvals have very different audit needs.&lt;/li&gt;
&lt;li&gt;Build the step-level audit list before choosing the architecture, so the cost conversation has actual requirements behind it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which step in your current agent would you be least comfortable explaining to a customer without a stored rationale?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sources referenced: Hacker News discussion on hybrid reasoning agents&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31725/hiding-agent-reasoning-costs-you-your-debug-trail</guid>
<pubDate>Mon, 19 Oct 2026 13:22:01 +0000</pubDate>
</item>
<item>
<title>Gemini API Backup Key 503 Bounded Failover</title>
<link>https://coderlegion.com/31674/gemini-api-backup-key-503-bounded-failover</link>
<description>&lt;p&gt;A configured backup credential often remains unused when application logic only switches keys after a quota response, allowing a temporary service failure to skip an available recovery path.&lt;/p&gt;
&lt;p&gt;Quick solution:&lt;br&gt;
Configure your fallback wrapper to catch HTTP 429, 500, 502, 503, and 504 responses before cycling to the next available credential slot.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BoundedFailoverClient:
    def __init__(self, credentials):
        self.credentials = credentials
        self.current_index = 0

    def execute(self, request_payload):
        attempts = 0
        while attempts &amp;lt; len(self.credentials):
            try:
                response = self._send(self.credentials[self.current_index], request_payload)
                if response.status_code in [429, 500, 502, 503, 504]:
                    raise TransientError(f&quot;Status {response.status_code}&quot;)
                return response
            except TransientError:
                attempts += 1
                self.current_index = (self.current_index + 1) % len(self.credentials)
        raise AllProvidersFailedError(&quot;All backup credentials exhausted.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prerequisites include maintaining separate project credentials and ensuring request errors remain terminal instead of triggering retries.&lt;/p&gt;
&lt;h3 id=&quot;which-response-classes-may-use-the-backup-chain-&quot;&gt;Which Response Classes May Use the Backup Chain?&lt;/h3&gt;
&lt;p&gt;The fallback chain should only engage for specific status codes that indicate capacity or availability issues rather than client-side malformations. Specifically, HTTP 429 (Too Many Requests), 500 (Internal Server Error), 502 (Bad Gateway), 503 (Service Unavailable), and 504 (Gateway Timeout) are eligible to trigger the backup chain as detailed in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://ai.google.dev/gemini-api/docs/troubleshooting&quot;&gt;Gemini API Troubleshooting Guide&lt;/a&gt;. Client-side request errors such as 400 Bad Request or 422 Unprocessable Entity must remain terminal and fail fast, similar to how developers handle invalid payloads when they &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/debug-http-422-validation-errors-safely/&quot;&gt;debug http 422 validation errors safely&lt;/a&gt; without triggering credential rotation.&lt;/p&gt;
&lt;h3 id=&quot;why-separate-projects-do-not-guarantee-recovery-from-a-service-outage&quot;&gt;Why Separate Projects Do Not Guarantee Recovery from a Service Outage&lt;/h3&gt;
&lt;p&gt;Isolating API keys across different Google Cloud or developer projects helps distribute quota limits, but it does not protect your application from a regional or global provider outage. If the underlying API infrastructure experiences a service disruption, secondary project credentials will encounter the same HTTP 503 or 504 errors. Developers must distinguish between project quota exhaustion, which is successfully mitigated by key rotation, and upstream availability failures where all projects are simultaneously impacted.&lt;/p&gt;
&lt;h3 id=&quot;how-tests-prove-one-bounded-chain-per-credential-and-no-secret-leakage&quot;&gt;How Tests Prove One Bounded Chain Per Credential and No Secret Leakage&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/how-to-build-robust-viewmodel-unit-tests/&quot;&gt;Unit tests&lt;/a&gt; must verify that the failover mechanism iterates through each configured key exactly once per &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/serverless-request-budget/&quot;&gt;request budget&lt;/a&gt; and never exposes sensitive tokens in error logs or exception messages. A proper test suite mocks the transport layer to return alternating 503 and 200 responses, asserting that the client successfully retries with the secondary key without printing the raw API secret to standard output.&lt;/p&gt;
&lt;h3 id=&quot;what-should-happen-when-all-providers-fail-without-weakening-publication-review-&quot;&gt;What Should Happen When All Providers Fail Without Weakening Publication Review?&lt;/h3&gt;
&lt;p&gt;When every configured credential in the chain exhausts its retry budget, the client must raise a terminal exception and halt execution rather than looping indefinitely. This strict boundary preserves publication review standards by ensuring that persistent failures surface immediately to monitoring systems instead of masking underlying infrastructure degradation.&lt;/p&gt;
&lt;h3 id=&quot;execution-checklist&quot;&gt;Execution Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Verify that terminal request errors immediately abort the execution loop.&lt;/li&gt;
&lt;li&gt;Confirm that HTTP 429, 500, 502, 503, and 504 status codes trigger the backup key rotation.&lt;/li&gt;
&lt;li&gt;Assert that secret tokens are scrubbed from all exception strings and log outputs.&lt;/li&gt;
&lt;li&gt;Ensure the total retry count matches the number of available credentials in your pool.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31674/gemini-api-backup-key-503-bounded-failover</guid>
<pubDate>Mon, 19 Oct 2026 09:14:39 +0000</pubDate>
</item>
<item>
<title>Timeouts, retries and streaming with the OpenAI Python SDK: the defaults that surprised me</title>
<link>https://coderlegion.com/31690/timeouts-retries-and-streaming-with-the-openai-python-sdk-the-defaults-that-surprised-me</link>
<description>&lt;p&gt;I run the same code against several OpenAI-compatible endpoints: hosted ones, a gateway, and a local server. Three SDK defaults caused most of my production issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The default read timeout is 10 minutes.&lt;/strong&gt; &lt;code&gt;openai.DEFAULT_TIMEOUT&lt;/code&gt; is &lt;code&gt;Timeout(connect=5.0, read=600, write=600, pool=600)&lt;/code&gt;. A hung upstream keeps a worker busy for up to 10 minutes. Set it explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os
from openai import OpenAI, Timeout

client = OpenAI(
    base_url=os.environ.get(&quot;LLM_BASE_URL&quot;, &quot;https://api.china-aiapi.com/v1&quot;),
    api_key=os.environ[&quot;LLM_API_KEY&quot;],
    timeout=Timeout(60.0, connect=5.0),  # 60 s read/write/pool, 5 s connect
    max_retries=2,                       # the default; shown so it's visible
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Retries multiply.&lt;/strong&gt; The SDK retries on connection errors, 408, 409, 429 and 5xx, with backoff, and honours &lt;code&gt;Retry-After&lt;/code&gt;. If your gateway or proxy &lt;em&gt;also&lt;/em&gt; retries, one slow request can turn into 3 × N upstream attempts. Pick one layer to own retries. If the server side already does it, use &lt;code&gt;max_retries=0&lt;/code&gt; in the client, or set it per call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;quick = client.with_options(max_retries=0, timeout=20.0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Streams are only retried before the first byte.&lt;/strong&gt; Once tokens are flowing, a dropped connection raises in the middle of your loop, and replaying the request would duplicate output the user has already seen. Handle that case yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from openai import APIConnectionError, APITimeoutError

def stream_answer(prompt: str) -&amp;gt; str:
    parts = []
    try:
        stream = client.chat.completions.create(
            model=&quot;MODEL_ID&quot;,
            messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: prompt}],
            stream=True,
            stream_options={&quot;include_usage&quot;: True},
        )
        for chunk in stream:
            if chunk.choices and chunk.choices[0].delta.content:
                parts.append(chunk.choices[0].delta.content)
                print(chunk.choices[0].delta.content, end=&quot;&quot;, flush=True)
            if chunk.usage:  # final chunk: choices is empty, usage is set
                print(f&quot;\n[{chunk.usage.total_tokens} tokens]&quot;)
    except (APIConnectionError, APITimeoutError):
        if parts:
            parts.append(&quot; [connection lost, answer incomplete]&quot;)
        else:
            raise  # nothing shown yet: safe to retry at a higher level
    return &quot;&quot;.join(parts)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;if chunk.choices&lt;/code&gt; check. With &lt;code&gt;include_usage&lt;/code&gt;, the last chunk has an empty &lt;code&gt;choices&lt;/code&gt; list, and &lt;code&gt;chunk.choices[0]&lt;/code&gt; throws &lt;code&gt;IndexError&lt;/code&gt; at the very end of an otherwise good answer.&lt;/p&gt;
&lt;p&gt;I'd like to hear how others split retries between client and gateway.&lt;/p&gt;
&lt;p&gt;Disclosure: I work on the APIGOTO team; APIGOTO is independent of OpenAI, Anthropic and Google. The default base URL above is ours (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.china-aiapi.com/grokbot&quot;&gt;www.china-aiapi.com&lt;/a&gt;), but everything here applies to any OpenAI-compatible endpoint.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31690/timeouts-retries-and-streaming-with-the-openai-python-sdk-the-defaults-that-surprised-me</guid>
<pubDate>Mon, 19 Oct 2026 09:14:23 +0000</pubDate>
</item>
<item>
<title>Lossless vs Lossy Compression: The Core Difference Explained</title>
<link>https://coderlegion.com/31606/lossless-vs-lossy-compression-the-core-difference-explained</link>
<description>&lt;h3 id=&quot;the-essence-of-compression-expressing-the-same-information-with-less-data&quot;&gt;The Essence of Compression: Expressing the Same Information with Less Data&lt;/h3&gt;
&lt;p&gt;Before diving into the distinction between lossless and lossy compression, we need to understand what compression actually does.&lt;/p&gt;
&lt;p&gt;At its core, compression is about &lt;strong&gt;expressing the same information with less data&lt;/strong&gt;. Any piece of data—whether text, images, audio, or video—contains &quot;redundancy.&quot; Redundancy refers to parts that can be removed without losing the meaning of the information. A compression algorithm's job is to identify and eliminate this redundancy.&lt;/p&gt;
&lt;p&gt;Consider a simple example. The character string &lt;code&gt;AAAAABBBCC&lt;/code&gt; occupies 10 bytes. But we can represent the same information more compactly by recording &quot;each character and how many times it repeats consecutively,&quot; yielding &lt;code&gt;5A3B2C&lt;/code&gt;—only 6 bytes. The data size drops by 40%, yet the meaning is identical. This is the most primitive form of compression, known as Run-Length Encoding (RLE).&lt;/p&gt;
&lt;p&gt;From an information theory perspective, Claude Shannon introduced the concept of &quot;information entropy&quot;: the minimum average encoding length of a data stream is bounded by its entropy. No lossless algorithm can break this theoretical limit. This means that re-compressing already-compressed data yields almost no benefit—the redundancy has already been stripped away.&lt;/p&gt;
&lt;p&gt;Understanding this essence reveals two fundamentally different technical paths: &lt;strong&gt;lossless compression&lt;/strong&gt; strives for perfect fidelity, while &lt;strong&gt;lossy compression&lt;/strong&gt; deliberately discards details that the human eye or ear cannot easily perceive, trading them for a much higher compression ratio.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lossless-compression-restoring-every-bit&quot;&gt;Lossless Compression: Restoring Every Bit&lt;/h3&gt;
&lt;p&gt;The core promise of lossless compression is that the decompressed data is &lt;strong&gt;byte-for-byte identical&lt;/strong&gt; to the original—not a single bit may differ. These algorithms eliminate only &quot;statistical redundancy&quot; and &quot;structural redundancy,&quot; never touching the semantic content of the information.&lt;/p&gt;
&lt;h4 id=&quot;deflate-the-most-widely-used-lossless-algorithm&quot;&gt;DEFLATE: The Most Widely Used Lossless Algorithm&lt;/h4&gt;
&lt;p&gt;DEFLATE is the algorithm underlying ZIP, gzip, zlib, and many other tools. It elegantly combines two classic techniques:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;LZ77&lt;/strong&gt;: Uses a sliding window to search for repeated substrings in already-processed data, replacing them with &quot;distance + length&quot; pointers. For example, the word &quot;compression&quot; appearing repeatedly in a text file can be replaced with a back-reference to its first occurrence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Huffman coding&lt;/strong&gt;: Applies variable-length encoding to the symbol stream produced by LZ77. Frequently occurring symbols get short codes; rare symbols get long codes. This pushes the encoding closer to the entropy limit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DEFLATE's strengths are its simplicity, speed, and universal compatibility—nearly every programming language's standard library includes an implementation.&lt;/p&gt;
&lt;h4 id=&quot;lzma-higher-ratios-through-context-modeling&quot;&gt;LZMA: Higher Ratios Through Context Modeling&lt;/h4&gt;
&lt;p&gt;LZMA (Lempel-Ziv-Markov chain Algorithm) builds on LZ77 with a much larger sliding window (up to several GB), a more refined probability model, and range coding. Compared to DEFLATE, LZMA typically achieves higher compression ratios, at the cost of slower compression speed and greater memory usage. The 7z format is based on LZMA.&lt;/p&gt;
&lt;p&gt;The key improvement in LZMA is that it does not rely on a static probability table. Instead, it dynamically predicts the probability of the next symbol based on context. This &quot;context modeling&quot; brings the encoding much closer to the theoretical entropy limit.&lt;/p&gt;
&lt;h4 id=&quot;png-filtering-preprocessing-for-image-data&quot;&gt;PNG Filtering: Preprocessing for Image Data&lt;/h4&gt;
&lt;p&gt;PNG is a lossless image format, but it does not apply DEFLATE directly to raw pixel values. Before compression, PNG applies a &quot;filter&quot; to each row of pixels to make the data more amenable to DEFLATE.&lt;/p&gt;
&lt;p&gt;A common filter stores the difference between each pixel and its neighbor to the left, above, or upper-left. Since natural images have locally similar pixels, these differences tend to cluster around zero. This transformed data has higher redundancy, allowing DEFLATE to compress it far more effectively.&lt;/p&gt;
&lt;p&gt;This also explains why ZIP cannot effectively compress JPEG images: JPEG is already highly compressed through DCT and entropy coding, leaving almost no statistical redundancy for DEFLATE to exploit. Running ZIP on a JPEG typically adds a few bytes of header overhead with no size reduction.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lossy-compression-trading-below-the-perception-threshold&quot;&gt;Lossy Compression: Trading Below the Perception Threshold&lt;/h3&gt;
&lt;p&gt;The central idea of lossy compression is that &lt;strong&gt;human perception has limits&lt;/strong&gt;. If the discarded details fall below the perceptual threshold, the visual or auditory difference is negligible—but the data reduction can be enormous.&lt;/p&gt;
&lt;h4 id=&quot;dct-the-mathematical-foundation-of-jpeg&quot;&gt;DCT: The Mathematical Foundation of JPEG&lt;/h4&gt;
&lt;p&gt;The Discrete Cosine Transform (DCT) is the heart of JPEG image compression. Its role is to convert an image from the &quot;spatial domain&quot; to the &quot;frequency domain.&quot;&lt;/p&gt;
&lt;p&gt;In the spatial domain, an image is represented by pixel brightness values. In the frequency domain, the image is decomposed into a sum of cosine wave components at different frequencies: low-frequency components represent large smooth areas, while high-frequency components represent edges and fine detail.&lt;/p&gt;
&lt;p&gt;The DCT itself is reversible and loses no information. The actual &quot;loss&quot; happens in the next step—quantization.&lt;/p&gt;
&lt;h4 id=&quot;quantization-the-source-of-loss&quot;&gt;Quantization: The Source of Loss&lt;/h4&gt;
&lt;p&gt;Quantization maps high-precision DCT coefficients to a limited set of discrete values. In practice, each coefficient is divided by a value from a quantization table and then rounded to an integer. Because the human eye is less sensitive to high-frequency detail, the quantization table applies larger divisors to high-frequency coefficients, causing many of them to become zero.&lt;/p&gt;
&lt;p&gt;This step causes irreversible information loss—once a coefficient is rounded to zero, it cannot be recovered. But it also creates massive compression opportunity: long runs of zeros can be encoded very efficiently with run-length encoding.&lt;/p&gt;
&lt;p&gt;Designing the quantization table is an art: larger divisors yield higher compression but more visible quality loss. This is exactly what the JPEG quality parameter (e.g., quality=80) controls—it scales the quantization table up or down.&lt;/p&gt;
&lt;h4 id=&quot;inter-frame-prediction-the-key-to-video-compression&quot;&gt;Inter-Frame Prediction: The Key to Video Compression&lt;/h4&gt;
&lt;p&gt;Video compression goes a step beyond still images. Consecutive frames in a video often change very little (for example, a static background with a moving person), so there is no need to store every frame in full.&lt;/p&gt;
&lt;p&gt;Inter-frame prediction uses &quot;motion estimation&quot; to find the displacement between the current frame and a reference frame, storing only &quot;motion vectors&quot; and &quot;residuals.&quot; This elimination of temporal redundancy allows video compression ratios to reach hundreds or even thousands to one—far beyond what single-frame image compression can achieve.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lossless-vs-lossy-comparison-diagram&quot;&gt;Lossless vs Lossy: Comparison Diagram&lt;/h3&gt;
&lt;p&gt;The flowchart below illustrates the core branches and typical applications of the two compression families:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/hwt4ff1zij0vf54zqj03.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;dct-principle-diagram&quot;&gt;DCT Principle Diagram&lt;/h3&gt;
&lt;p&gt;The diagram below simplifies how DCT transforms an 8x8 pixel block from the spatial domain to the frequency domain, then preserves low frequencies while discarding high frequencies through quantization:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/et3p2djbwk12elm97aqr.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;code-examples&quot;&gt;Code Examples&lt;/h3&gt;
&lt;h4 id=&quot;lossless-compression-with-zlib&quot;&gt;Lossless Compression with zlib&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import zlib

# Original text data with heavy repetition
original = b&quot;compression compression compression &quot; * 50

# Compress
compressed = zlib.compress(original, level=9)
print(f&quot;Original size: {len(original)} bytes&quot;)
print(f&quot;Compressed size: {len(compressed)} bytes&quot;)
print(f&quot;Ratio: {len(original) / len(compressed):.2f}x&quot;)

# Decompress and verify the data is identical
decompressed = zlib.decompress(compressed)
assert decompressed == original, &quot;Data mismatch!&quot;
print(&quot;Verification passed: decompressed data is identical to original&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running this code, you will typically see a compression ratio above 10x, and the decompressed data is byte-for-byte identical to the original. This is what &quot;lossless&quot; means.&lt;/p&gt;
&lt;h4 id=&quot;lossy-compression-with-pil&quot;&gt;Lossy Compression with PIL&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from PIL import Image
import io

# Create a test image rich in high-frequency detail
img = Image.new(&quot;RGB&quot;, (512, 512))
pixels = img.load()
for x in range(512):
    for y in range(512):
        # Generate a pattern with high-frequency detail
        pixels[x, y] = ((x * 7) % 256, (y * 5) % 256, ((x + y) * 3) % 256)

# Save as lossless PNG
img.save(&quot;test_lossless.png&quot;)
png_size = len(open(&quot;test_lossless.png&quot;, &quot;rb&quot;).read())
print(f&quot;PNG (lossless) size: {png_size} bytes&quot;)

# Save as lossy JPEG with quality 20 to amplify the effect
buffer = io.BytesIO()
img.save(buffer, format=&quot;JPEG&quot;, quality=20)
jpeg_size = buffer.tell()
print(f&quot;JPEG (lossy) size: {jpeg_size} bytes&quot;)
print(f&quot;Ratio: {png_size / jpeg_size:.2f}x&quot;)

# Reload the JPEG and compare pixel differences
buffer.seek(0)
img_jpeg = Image.open(buffer)
diff_count = sum(
    1 for x in range(512) for y in range(512)
    if img.getpixel((x, y)) != img_jpeg.getpixel((x, y))
)
print(f&quot;Pixels that changed: {diff_count} / {512 * 512}&quot;)
print(&quot;Conclusion: JPEG compression alters pixel values irreversibly&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code demonstrates the hallmark of lossy compression: a smaller file, but with pixel values changed irreversibly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;comparison-table&quot;&gt;Comparison Table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Dimension &lt;/th&gt;&lt;th&gt;Lossless Compression &lt;/th&gt;&lt;th&gt;Lossy Compression&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Core Principle&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Eliminates statistical &amp;amp; structural redundancy &lt;/td&gt;&lt;td&gt;Discards info below perception threshold&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fidelity&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;100% reversible, identical after decompression &lt;/td&gt;&lt;td&gt;Irreversible, unrecoverable loss&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical Algorithms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;DEFLATE, LZMA, Brotli, Zstandard &lt;/td&gt;&lt;td&gt;DCT+quantization, wavelet, MDCT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Key Techniques&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;LZ77, Huffman coding, range coding &lt;/td&gt;&lt;td&gt;Transform coding, quantization, prediction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical Applications&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;ZIP, GZIP, 7Z, PNG, FLAC &lt;/td&gt;&lt;td&gt;JPEG, WebP, H.264, MP3, AAC&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best Suited For&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Text, source code, archives, medical imaging &lt;/td&gt;&lt;td&gt;Photos, video, music, streaming&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical Ratio&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Usually 2x to 5x &lt;/td&gt;&lt;td&gt;Usually 10x to 100x or higher&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Not Suitable For&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Already-compressed data (e.g., JPEG files) &lt;/td&gt;&lt;td&gt;Data requiring exact reproduction (e.g., legal text)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;q1-why-can-t-zip-compress-a-jpeg-image-&quot;&gt;Q1: Why can't ZIP compress a JPEG image?&lt;/h4&gt;
&lt;p&gt;By the time a JPEG is created, it has already gone through DCT, quantization, and entropy coding. The statistical redundancy in the data has been almost entirely eliminated. DEFLATE cannot find exploitable repetition patterns, so running ZIP on a JPEG not only fails to reduce the size but may actually add a few bytes of header overhead.&lt;/p&gt;
&lt;h4 id=&quot;q2-is-png-always-larger-than-jpeg-for-the-same-image-&quot;&gt;Q2: Is PNG always larger than JPEG for the same image?&lt;/h4&gt;
&lt;p&gt;Not necessarily. For images with large areas of solid color or a limited palette (such as logos, icons, or screenshots), PNG's filter-plus-DEFLATE combination is often smaller than JPEG and preserves perfect quality. But for color-rich natural photographs, JPEG's DCT-plus-quantization typically achieves compression ratios above 10x, making it far smaller than PNG.&lt;/p&gt;
&lt;h4 id=&quot;q3-can-you-repeatedly-compress-the-same-file-with-a-lossless-algorithm-&quot;&gt;Q3: Can you repeatedly compress the same file with a lossless algorithm?&lt;/h4&gt;
&lt;p&gt;No. Each pass of lossless compression pushes the data closer to its entropy limit. After the first pass, redundancy is largely gone, and a second pass has almost nothing left to exploit—it may even grow slightly due to added metadata. This is why &quot;compressing an already-compressed file&quot; is futile.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary-1&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Lossless and lossy compression are not opposing camps but optimal choices made for different information characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lossless compression&lt;/strong&gt; serves data where &quot;not a single bit can be wrong&quot;—text, code, archives. It uses techniques like LZ77 and Huffman coding to eliminate statistical redundancy while guaranteeing perfect reconstruction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lossy compression&lt;/strong&gt; serves details &quot;the eye and ear cannot perceive&quot;—photos, video, music. It uses DCT, quantization, and inter-frame prediction to make trade-offs below the perception threshold, exchanging controlled information loss for order-of-magnitude improvements in compression ratio.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding the fundamental difference between these two paths is the cornerstone of mastering all compression technology. In subsequent articles, we will dive into the specific algorithm implementations along each path, understanding the elegance of compression line by line.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31606/lossless-vs-lossy-compression-the-core-difference-explained</guid>
<pubDate>Sun, 18 Oct 2026 11:31:34 +0000</pubDate>
</item>
<item>
<title>Escaping the Automation Tax: Migrating AI Agent Workflows to Self-Hosted n8n</title>
<link>https://coderlegion.com/31599/escaping-the-automation-tax-migrating-ai-agent-workflows-to-self-hosted-n8n</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;⚡ TL;DR — Ready-to-Deploy Automation Blueprints:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Turnkey Production Package (This Guide):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://whop.com/mr-studio-7faa/cloud-automation-to-n8n-migration-kit-self-hosted-ai-workflows&quot;&gt;Instant Access on Whop&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/valjxi&quot;&gt;Download on Gumroad&lt;/a&gt; &lt;em&gt;(Use code &lt;strong&gt;EARLYBIRD&lt;/strong&gt; for -20%)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;🎁 &lt;strong&gt;Free Starter Blueprints (0€):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/ai-agent-memory-rag-sync-notion-airtable-n8n-20260929&quot;&gt;n8n AI Memory &amp;amp; Notion/Airtable RAG Sync&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/programmatic-seo-markdown-generator-python-20260929&quot;&gt;Programmatic SEO Engine (Python)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💎 &lt;strong&gt;MR Studio Lifetime All-Access Vault:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/depoud&quot;&gt;Get All Current &amp;amp; Future Workflows (€39 One-Time)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;If you’ve built AI agents on usage-based cloud automation platforms (like Zapier or Make), you already know the fatal flaw: &lt;strong&gt;success is penalized&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;When you have an AI agent that scrapes a site, chunks the data, loops through an LLM to extract insights, and routes the formatted data to a CRM, you aren't executing one task. You are executing 15 to 50 tasks &lt;em&gt;per run&lt;/em&gt;. At scale, paying per-execution cripples your margins.&lt;/p&gt;
&lt;p&gt;The solution is migrating complex, stateful workflows to a self-hosted environment like &lt;strong&gt;n8n&lt;/strong&gt;. But migrating often causes pipeline downtime and requires rebuilding execution logic from scratch.&lt;/p&gt;
&lt;p&gt;In this guide, I’ll walk you through the architecture, code, and deployment strategies to migrate a webhook-to-database AI enrichment pipeline to self-hosted n8n without dropping a single payload.&lt;/p&gt;
&lt;h3 id=&quot;1-the-bottleneck-why-cloud-automation-fails-ai-agents&quot;&gt;1. The Bottleneck: Why Cloud Automation Fails AI Agents&lt;/h3&gt;
&lt;p&gt;Commercial solutions are fantastic for simple &lt;code&gt;Trigger -&amp;gt; Action&lt;/code&gt; workflows. But AI workflows are rarely linear. They require:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Loops &amp;amp; Pagination:&lt;/strong&gt; Processing large documents or paginated API responses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Management:&lt;/strong&gt; Remembering context between retries if an LLM API times out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High-Volume Webhooks:&lt;/strong&gt; Catching inbound data from forms or scrapers at high concurrency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you run a 20-step AI workflow 5,000 times a month on a commercial cloud platform, you're paying for 100,000 tasks. On a $20/month cloud VPS running n8n, that exact same workload costs exactly $20. &lt;/p&gt;
&lt;h3 id=&quot;2-the-architecture-webhook-to-crm-pipeline&quot;&gt;2. The Architecture: Webhook-to-CRM Pipeline&lt;/h3&gt;
&lt;p&gt;To make this practical, let's architect a robust B2B Lead Enrichment Pipeline. The pipeline receives a raw email and company name, scrapes the company website, uses an LLM to determine the company's tech stack, and upserts it into a PostgreSQL database (or CRM).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Flow:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Webhook Node:&lt;/strong&gt; Catches the inbound POST request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Node (JavaScript):&lt;/strong&gt; Sanitizes the domain and validates the payload schema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP Node:&lt;/strong&gt; Fetches the target company's homepage HTML.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Node (Python):&lt;/strong&gt; Parses the HTML and structures the LLM prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAI / Advanced AI Node:&lt;/strong&gt; Extracts the tech stack and outputs strict JSON.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL Node:&lt;/strong&gt; Upserts the data based on the domain key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Trigger Workflow:&lt;/strong&gt; A global error handler that catches rate limits and queues retries.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;3-the-code-amp-logic&quot;&gt;3. The Code &amp;amp; Logic&lt;/h3&gt;
&lt;p&gt;One of n8n's superpowers is the Code Node, which allows you to run custom JavaScript or Python. Here is how we handle domain sanitization and prompt structuring.&lt;/p&gt;
&lt;h4 id=&quot;step-2-payload-validation-amp-domain-sanitization-javascript-&quot;&gt;Step 2: Payload Validation &amp;amp; Domain Sanitization (JavaScript)&lt;/h4&gt;
&lt;p&gt;Drop this into an n8n Code node to clean messy inbound webhook data before it hits your expensive scraping/LLM steps.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// n8n Code Node: Sanitize Domain
const items = $input.all();
const sanitizedItems = [];

for (const item of items) {
  let rawEmail = item.json.body.email || &quot;&quot;;
  let rawWebsite = item.json.body.website || &quot;&quot;;
  
  // Extract domain from email if website is missing
  let domain = rawWebsite;
  if (!domain &amp;amp;&amp;amp; rawEmail.includes(&quot;@&quot;)) {
    domain = rawEmail.split(&quot;@&quot;)[1];
  }
  
  // Strip protocols and www
  if (domain) {
    domain = domain.replace(&quot;https://&quot;, &quot;&quot;).replace(&quot;http://&quot;, &quot;&quot;).replace(&quot;www.&quot;, &quot;&quot;).split(&quot;/&quot;)[0];
  }

  if (!domain) {
    throw new Error(&quot;Validation Failed: No valid domain found.&quot;);
  }

  sanitizedItems.push({
    json: {
      ...item.json.body,
      clean_domain: domain
    }
  });
}

return sanitizedItems;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-4-prompt-structuring-python-&quot;&gt;Step 4: Prompt Structuring (Python)&lt;/h4&gt;
&lt;p&gt;Using n8n's Python support, we can cleanly truncate the HTML and structure a deterministic prompt for the LLM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# n8n Code Node (Python): Prepare LLM Payload
import re

def clean_html(raw_html):
    # Remove scripts and styles
    clean = re.sub('&amp;lt;script.*?&amp;lt;/script&amp;gt;', '', raw_html, flags=re.DOTALL)
    clean = re.sub('&amp;lt;style.*?&amp;lt;/style&amp;gt;', '', clean, flags=re.DOTALL)
    # Strip remaining tags
    clean = re.sub('&amp;lt;.*?&amp;gt;', ' ', clean)
    return ' '.join(clean.split())[:8000] # Truncate to fit context window

items = _input.all()
out = []

for item in items:
    html_content = item.json.get('html_data', '')
    cleaned_text = clean_html(html_content)
    
    system_prompt = &quot;You are a B2B data extraction agent. Analyze the text and return a JSON object with 'tech_stack' (array) and 'industry' (string).&quot;
    
    out.append({
        &quot;json&quot;: {
            &quot;system_prompt&quot;: system_prompt,
            &quot;user_prompt&quot;: &quot;Website Text: &quot; + cleaned_text,
            &quot;domain&quot;: item.json.get('clean_domain')
        }
    })

return out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-deployment-amp-performance-handling-the-chaos&quot;&gt;4. Deployment &amp;amp; Performance: Handling the Chaos&lt;/h3&gt;
&lt;p&gt;When migrating to self-hosted n8n, you are now responsible for execution resilience. Here are the three non-negotiables for production AI workflows:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Built-in Retry Logic &amp;amp; Rate Limits&lt;/strong&gt;&lt;br&gt;
LLM providers rate-limit you. In your n8n HTTP or AI nodes, open the &lt;strong&gt;Settings&lt;/strong&gt; tab. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;On Error&lt;/strong&gt; to &lt;code&gt;Continue (using Error Output)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use an &lt;code&gt;If&lt;/code&gt; node to check for HTTP status &lt;code&gt;429&lt;/code&gt; (Too Many Requests).&lt;/li&gt;
&lt;li&gt;Route the error to a &lt;strong&gt;Wait Node&lt;/strong&gt; set to delay for 60 seconds, then loop it back to the AI node.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Workflow Timeouts&lt;/strong&gt;&lt;br&gt;
Scraping and AI inference can take minutes. Go to your Workflow Settings and increase the &lt;strong&gt;Timeout limit&lt;/strong&gt; to at least 15 minutes to prevent the workflow from being killed mid-generation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Global Error Routing&lt;/strong&gt;&lt;br&gt;
Create a separate workflow starting with an &lt;strong&gt;Error Trigger&lt;/strong&gt; node. If any node in your main pipeline hard-fails, this workflow catches the execution ID, the error trace, and the exact payload, alerting you via Slack or logging it in your database so no leads are dropped during migration.&lt;/p&gt;
&lt;h3 id=&quot;5-conclusion-amp-ready-to-use-workflow&quot;&gt;5. Conclusion &amp;amp; Ready-to-use Workflow&lt;/h3&gt;
&lt;p&gt;Migrating from commercial cloud automation to self-hosted n8n is the single highest ROI engineering task you can do for your AI agent infrastructure. The cost scales asymptotically to zero, and you gain absolute control over your state, timeouts, and logic.&lt;/p&gt;
&lt;p&gt;You can absolutely rebuild your infrastructure from scratch using the logic and code blocks above. However, if you want to skip the 20+ hours of debugging node configurations, writing regex, and testing error-handling loops, I've packaged my exact production setups.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Cloud Automation to n8n Migration Kit: Self-Hosted AI Workflows&lt;/strong&gt; includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;5 Production-Ready n8n AI Agent Workflow Templates&lt;/strong&gt; (JSON imports).&lt;/li&gt;
&lt;li&gt;Drop-in replacements for complex webhook-to-database pipelines.&lt;/li&gt;
&lt;li&gt;Pre-configured retry logic, rate-limit management, and error handling.&lt;/li&gt;
&lt;li&gt;Plug-and-play JavaScript modules for n8n Code Nodes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can grab the turnkey package and import the JSON directly into your n8n instance today:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instant Access on Whop:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://whop.com/mr-studio-7faa/cloud-automation-to-n8n-migration-kit-self-hosted-ai-workflows&quot;&gt;Get the Migration Kit Here&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct Download on Gumroad:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/valjxi&quot;&gt;Download Here&lt;/a&gt; &lt;em&gt;(Use promo code &lt;strong&gt;EARLYBIRD&lt;/strong&gt; at checkout for 20% off).&lt;/em&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stop paying the automation tax. Self-host your agents, own your infrastructure, and scale your AI workflows without punishing your margins.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-d-autres-outils-mr-studio-qui-pourraient-vous-int-resser-&quot;&gt;🛠️ D'autres outils MR Studio qui pourraient vous intéresser :&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/crixzs&quot;&gt;n8n Micro-SaaS Agent Farm &amp;amp; Sub-Agent Orchestration Engine&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/avdqx&quot;&gt;Local Repo Refactoring Agent: AI Codebase Migration Engine&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/twfkrv&quot;&gt;Vision-Driven Android QA Automation Engine — Python Pipeline&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31599/escaping-the-automation-tax-migrating-ai-agent-workflows-to-self-hosted-n8n</guid>
<pubDate>Sun, 18 Oct 2026 10:52:53 +0000</pubDate>
</item>
<item>
<title>I Replaced $40/mo AI Job Trackers with a Local Python &amp; Web Scraping Command Center (Zero API Key)</title>
<link>https://coderlegion.com/31446/replaced-40-mo-ai-job-trackers-with-local-python-web-scraping-command-center-zero-api-key</link>
<description>&lt;p&gt;If you have looked for a remote role recently, you know the routine.&lt;/p&gt;
&lt;p&gt;You wake up, open 40 browser tabs across Ashby, Lever, Greenhouse, Remotive, Welcome to the Jungle, We Work Remotely, and the monthly Hacker News &lt;em&gt;Who is Hiring&lt;/em&gt; thread. Half the &quot;remote&quot; listings secretly say &lt;em&gt;US Only - Must reside in Texas&lt;/em&gt; in paragraph four. Another third are ghost jobs reposted every Tuesday.&lt;/p&gt;
&lt;p&gt;When I looked for tools to automate this, every commercial &quot;AI job copilot&quot; had the same three problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Another $29 to $49/month subscription&lt;/strong&gt; at the exact moment you are looking for work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your resume and personal data live on someone else's cloud.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The double-billing trap:&lt;/strong&gt; if you look at open-source tools, almost all of them require a paid, pay-per-token OpenAI or Anthropic API key, even though you &lt;em&gt;already&lt;/em&gt; pay $20/month for a ChatGPT Plus, Claude Pro, or Gemini subscription.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wanted something local, fast, and private that ran on my own machine. So over the last few weeks, I built and open-sourced &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/manny-ruesch/remote-job-hunter&quot;&gt;Remote Job Hunter (&lt;code&gt;rjh&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/manny-ruesch/remote-job-hunter/main/docs/assets/noir-webling-demo.gif&quot; alt=&quot;Remote Job Hunter Command Center Demo&quot;&gt;&lt;/p&gt;
&lt;p&gt;It is a local command center written in Python (standard library for the core engine + FastAPI and SQLite WAL) that scans 51+ job sources in parallel, scores every offer against a single &lt;code&gt;profile.yaml&lt;/code&gt;, generates a 1-page ATS-safe PDF resume per company, and works &lt;strong&gt;even if you do not have an API key&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt; I just pushed &lt;code&gt;v0.2&lt;/code&gt; to GitHub, and &lt;strong&gt;I am looking for developers and job seekers to test it and tear it apart&lt;/strong&gt; so I can make &lt;code&gt;v0.3&lt;/code&gt; better.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;built-for-any-profession-tech-or-non-tech-remote-or-on-site-&quot;&gt;Built for Any Profession (Tech or Non-Tech, Remote or On-Site)&lt;/h3&gt;
&lt;p&gt;Despite the repo name (&lt;code&gt;remote-job-hunter&lt;/code&gt;), the engine is completely domain-agnostic. It does not assume you are a software engineer, and it does not force you to look for remote-only roles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;search:                                    # A nurse in Lyon (on-site)                # A remote RevOps / Python engineer
  titles: {primary: [infirmier, IDE]}      #   healthcare, finance, sales, HR...      #   [automation engineer, revops]
  work_mode: {remote: any, cities: [Lyon]} #   on-site, hybrid, or 100% remote        #   {remote: required}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whether you are a nurse in Lyon (&lt;code&gt;profile.non-tech.example.yaml&lt;/code&gt;), an accountant in Paris, a sales manager, or a backend engineer, dropping your PDF resume into the Command Center automatically extracts your domain skills and calibrates the search and 1-page ATS resume generator for your field.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-wait-why-do-i-need-an-api-key-realization&quot;&gt;1. The &quot;Wait, why do I need an API key?&quot; realization&lt;/h3&gt;
&lt;p&gt;When I shared the first prototype with a friend who is a nurse in Lyon (not a developer), she asked a simple question: &lt;em&gt;&quot;I already have ChatGPT on my phone. Why do I have to create a developer account and put in a credit card for an API key?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;She was right. API keys are great for unattended cron jobs, terrible for normal people.&lt;/p&gt;
&lt;p&gt;That conversation led to &lt;strong&gt;Zero-API-Key Manual Mode&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;  51+ job sources ─▶ deterministic heuristic filter ─▶ 10 offers bundled into ONE prompt
                                                                      │
     scores, pitches &amp;amp; PoW diagrams ◀─ paste JSON back ◀─ paste into ChatGPT / Claude / Gemini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is how it works in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;rjh scan&lt;/code&gt; pulls offers from all your configured sources in parallel and runs a fast, deterministic local scorer (&lt;code&gt;rjh/heuristic.py&lt;/code&gt;) that checks title match, location eligibility (&lt;code&gt;Europe&lt;/code&gt;, &lt;code&gt;Global&lt;/code&gt;, &lt;code&gt;US Restricted&lt;/code&gt;), seniority, language, and your custom hard rules.&lt;/li&gt;
&lt;li&gt;If you have an API key (&lt;code&gt;Gemini&lt;/code&gt;, &lt;code&gt;OpenAI&lt;/code&gt;, &lt;code&gt;Anthropic&lt;/code&gt;, &lt;code&gt;Groq&lt;/code&gt;, &lt;code&gt;Mistral&lt;/code&gt;, &lt;code&gt;OpenRouter&lt;/code&gt;) or a local &lt;code&gt;Ollama&lt;/code&gt; model, it evaluates the top candidates automatically.&lt;/li&gt;
&lt;li&gt;If you &lt;strong&gt;don't&lt;/strong&gt; have an API key (or if your free quota hits &lt;code&gt;429&lt;/code&gt;), it automatically falls back to Manual Mode. You click &lt;strong&gt;Export a batch&lt;/strong&gt; in the UI, paste the prompt into the ChatGPT/Claude/Gemini tab you already use, and paste the JSON reply back.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;handling-truncated-chat-responses-without-losing-work&quot;&gt;Handling truncated chat responses without losing work&lt;/h4&gt;
&lt;p&gt;Anyone who has pasted a large prompt into a web chat knows the main failure mode: the model stops generating at token 3,800 and leaves you with half a JSON array.&lt;/p&gt;
&lt;p&gt;Instead of failing with &lt;code&gt;JSONDecodeError&lt;/code&gt;, the parser in &lt;code&gt;rjh/analysis.py&lt;/code&gt; salvages every completed JSON object from the truncated stream, saves those scores to SQLite immediately, and generates a follow-up ticket containing &lt;strong&gt;only the missing job IDs&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def salvage_json_objects(raw_text: str) -&amp;gt; list[dict]:
    &quot;&quot;&quot;Extracts all complete top-level JSON objects from a truncated LLM array.&quot;&quot;&quot;
    decoder = json.JSONDecoder()
    results = []
    idx = 0
    length = len(raw_text)

    while idx &amp;lt; length:
        brace = raw_text.find(&quot;{&quot;, idx)
        if brace == -1:
            break
        try:
            obj, end = decoder.raw_decode(raw_text, brace)
            if isinstance(obj, dict) and &quot;id&quot; in obj:
                results.append(obj)
            idx = end
        except json.JSONDecodeError:
            idx = brace + 1
    return results
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing gets lost, and you never re-run offers that were already scored.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-two-stage-triage-proof-of-work-architecture-diagrams&quot;&gt;2. Two-Stage Triage + Proof-of-Work Architecture Diagrams&lt;/h3&gt;
&lt;p&gt;Early on, I tried asking the LLM to score a job &lt;em&gt;and&lt;/em&gt; write a custom pitch in a single pass. It was slow, burned tokens on mediocre roles, and produced generic pitches.&lt;/p&gt;
&lt;p&gt;I split the pipeline into two distinct stages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stage 1 (Fast Triage):&lt;/strong&gt; Evaluates up to 20 pre-filtered jobs per batch. Returns a &lt;code&gt;fit_score&lt;/code&gt; (&lt;code&gt;0.0&lt;/code&gt; to &lt;code&gt;10.0&lt;/code&gt;), detected tech stack, and a 1-sentence reason.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stage 2 (Deep Dossier for scores &amp;gt;= 7.5):&lt;/strong&gt; Only for the roles worth applying to, the engine generates:&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;3-paragraph tailored pitch&lt;/strong&gt; grounded in your actual metrics from &lt;code&gt;profile.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Proof-of-Work (&lt;code&gt;Mermaid.js&lt;/code&gt;) architecture diagram&lt;/strong&gt; proposing a concrete technical workflow for that company's stack, rendered directly inside the modal so you can walk through it during an interview.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;short founder/hiring-manager DM&lt;/strong&gt; with a 1-click LinkedIn X-Ray search link.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-solving-the-ats-resume-problem-cv-tailor-py-&quot;&gt;3. Solving the ATS Resume Problem (&lt;code&gt;cv_tailor.py&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;When you apply on Workday, Taleo, Greenhouse, Lever, or Ashby, fancy two-column resumes with sidebars, progress bars, or floating boxes often get scrambled into unreadable text streams.&lt;/p&gt;
&lt;p&gt;On the other hand, plain &lt;code&gt;.txt&lt;/code&gt;-style resumes look awful when a human recruiter actually opens the PDF.&lt;/p&gt;
&lt;p&gt;I built &lt;code&gt;cv_tailor.py&lt;/code&gt; to solve both at once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100% single-column linear DOM flow&lt;/strong&gt; (&lt;code&gt;Header -&amp;gt; Summary -&amp;gt; Core Stack -&amp;gt; Experience -&amp;gt; Education&lt;/code&gt;) so legacy and modern ATS parsers read every line in the exact right order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Editorial typography&lt;/strong&gt; (&lt;em&gt;Newsreader&lt;/em&gt; serif headers + &lt;em&gt;Plus Jakarta Sans&lt;/em&gt; body) that fits strictly on &lt;strong&gt;1 page A4&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero hallucination rule:&lt;/strong&gt; It extracts the exact technical keywords from the job description (&lt;code&gt;n8n&lt;/code&gt;, &lt;code&gt;Python&lt;/code&gt;, &lt;code&gt;PostgreSQL&lt;/code&gt;, &lt;code&gt;HubSpot&lt;/code&gt;, &lt;code&gt;REST APIs&lt;/code&gt;), reorders your existing bullet points so the most relevant ones sit at the top, and bolds matching terms without inventing skills or metrics you don't have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in 7-point ATS self-audit:&lt;/strong&gt; Every generated PDF is inspected with &lt;code&gt;pypdf&lt;/code&gt; before returning a score (&lt;code&gt;100/100&lt;/code&gt;, 1 page A4 verified) and automatically syncs with the 1-click Playwright form pre-filler.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-killing-my-own-cyberpunk-terminal-ui&quot;&gt;4.Killing my own &quot;Cyberpunk Terminal&quot; UI&lt;/h3&gt;
&lt;p&gt;I have a confession: the first version of the dashboard looked like a 1990s hacker movie. Everything was neon green, uppercase monospace, glowing text shadows, &lt;code&gt;[ BRACKETS EVERYWHERE ]&lt;/code&gt;, and emojis on every button.&lt;/p&gt;
&lt;p&gt;After staring at it for three days, my eyes hurt. A tool you use every morning shouldn't feel like a video game HUD.&lt;/p&gt;
&lt;p&gt;I stripped out all the neon glow, brackets, and fake hacker jargon, and rebuilt the interface around a calm &lt;strong&gt;Linear / Raycast / Editorial&lt;/strong&gt; aesthetic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dark Mode (&lt;code&gt;#09090B&lt;/code&gt;)&lt;/strong&gt; and &lt;strong&gt;Warm Paper Light Mode (&lt;code&gt;#FAF8F5&lt;/code&gt;)&lt;/strong&gt; switched via the browser View Transitions API circular ripple.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plus Jakarta Sans&lt;/strong&gt; for UI legibility, &lt;strong&gt;Newsreader&lt;/strong&gt; for editorial hooks, and &lt;strong&gt;JetBrains Mono (&lt;code&gt;tabular-nums&lt;/code&gt;)&lt;/strong&gt; strictly reserved for scores, counters, and keyboard shortcuts (&lt;code&gt;J&lt;/code&gt; / &lt;code&gt;K&lt;/code&gt; to navigate cards, &lt;code&gt;Enter&lt;/code&gt; for details, &lt;code&gt;A&lt;/code&gt; to pre-fill, &lt;code&gt;/&lt;/code&gt; to search).&lt;/li&gt;
&lt;li&gt;I kept our mascot (&lt;em&gt;Noir Webling&lt;/em&gt;) and the interactive dual-layer HTML5 Canvas spider web in the background, toned down into a subtle horology-inspired filigree with an elastic silk thread that connects to whichever job card you are inspecting.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-in-60-seconds&quot;&gt;Try it in 60 seconds&lt;/h3&gt;
&lt;p&gt;The entire project is MIT-licensed and runs locally on Linux, macOS, and Windows (Python 3.11+):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/manny-ruesch/remote-job-hunter.git
cd remote-job-hunter
pip install -e .

rjh init      # 4 quick questions (creates your profile.yaml)
rjh doctor    # verifies your sources, SQLite WAL database, and optional LLM key
rjh serve     # opens the Command Center on http://127.0.0.1:5070
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt; GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/manny-ruesch/remote-job-hunter&quot;&gt;https://github.com/manny-ruesch/remote-job-hunter&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;i-need-your-feedback-help-me-break-v0-2-&quot;&gt;I Need Your Feedback (Help Me Break &lt;code&gt;v0.2&lt;/code&gt;!)&lt;/h3&gt;
&lt;p&gt;This is a &lt;code&gt;v0.2&lt;/code&gt; release, and I want to shape the roadmap around real usage rather than guessing in a vacuum.&lt;/p&gt;
&lt;p&gt;If you have 3 minutes to test it out, I would love your honest feedback in the comments (or via a GitHub Issue):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Try &lt;code&gt;rjh init&lt;/code&gt; on your own job search&lt;/strong&gt; (whether you are looking for Backend, DevOps, AI/Automation, Frontend, or even a non-tech role). Did the heuristic scorer rank offers the way you expected, or did noisy offers slip through?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the Zero-API-Key Manual Mode&lt;/strong&gt; with your regular ChatGPT / Claude / Gemini chat. Was the copy-paste flow smooth, or is there friction I should remove?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the 1-Page ATS CV Tailor&lt;/strong&gt; on a real job offer. How did the generated PDF look with your profile?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which job boards or regional sources are missing?&lt;/strong&gt; Right now it ships with Greenhouse, Ashby, Lever, Welcome to the Jungle, Remotive, We Work Remotely, Jobicy, Adzuna, and Hacker News. Tell me which board you check every week and I will add a connector for it in &lt;code&gt;v0.3&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you find this useful, a ⭐ on &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/manny-ruesch/remote-job-hunter&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt; helps a ton, and every bug report or critique in the comments below goes straight into the next release!&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31446/replaced-40-mo-ai-job-trackers-with-local-python-web-scraping-command-center-zero-api-key</guid>
<pubDate>Fri, 16 Oct 2026 19:26:24 +0000</pubDate>
</item>
<item>
<title>What Are APIs? And How Do They Work?</title>
<link>https://coderlegion.com/31206/what-are-apis-and-how-do-they-work</link>
<description>&lt;p&gt;&lt;em&gt;A beginner-friendly guide, with a bit of Python and a lot of food analogies.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Imagine you're building a weather application, and it needs to show the current temperature in Mutare. There's just one problem: you don't own any weather sensors, and you definitely don't have a meteorological database lying around. Building that infrastructure yourself would be a huge project. Why reinvent the wheel? So you borrow the data from a service that already has it.&lt;/p&gt;
&lt;p&gt;The tool that makes this possible is something developers use all the time, often without noticing: an &lt;strong&gt;API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Welcome to another article, where we talk about APIs. Why they're needed, what they are, how they work, types of APIs, and show an example of a code block using an API. &lt;/p&gt;
&lt;p&gt;In simple terms, an &lt;strong&gt;API&lt;/strong&gt; is a defined interface that lets one piece of software interact with another. If you like, think of APIs as the cellphones of applications: they give programs a set way to call each other up and ask for things, like: &quot;Hey, can I have all the PDF documents on machine learning for beginners?&quot;&lt;/p&gt;
&lt;p&gt;In this article, we'll cover what APIs are, how they work, how to make your first API request in Python, the main types you'll hear about, and five beginner projects to practise on.&lt;/p&gt;
&lt;h3 id=&quot;why-do-applications-need-to-communicate-&quot;&gt;Why Do Applications Need to Communicate?&lt;/h3&gt;
&lt;p&gt;Modern applications rarely work alone. They depend on data and services provided by other systems.&lt;/p&gt;
&lt;p&gt;Take a food delivery app. It needs restaurant menus, a payment service, a map to show your location, and live tracking so you can follow your food as it's being delivered. Each job belongs to a different system, and the app connects to them instead of building everything itself.&lt;/p&gt;
&lt;p&gt;The same is true for AI applications. A chatbot might send a prompt to an AI model through one API, then fetch relevant information from a database through another, and combine both to write its answer. Each part has one responsibility, and each is reached through its own interface.&lt;/p&gt;
&lt;p&gt;Now let's bring this closer to home. In my current capstone project, we have five group members, each with different roles and strengths. We can't just disappear into our corners and hope everything magically fits together at the end. We need agreed rules: who builds what, what each piece takes in, and what it hands back.&lt;/p&gt;
&lt;p&gt;Software components need the same thing. Without a clearly defined way to talk to each other, they can't reliably work together. That defined way of making requests and passing information is the API.&lt;/p&gt;
&lt;h3 id=&quot;what-is-an-api-&quot;&gt;What Is an API?&lt;/h3&gt;
&lt;p&gt;Six months ago, I didn't even know what the letters stood for. Today I can tell you that API stands for &lt;strong&gt;Application Programming Interface&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here's the more precise definition. An API is a defined interface that allows one software component to interact with another. It specifies how requests can be made, what information or functionality is available, and how the receiving system communicates the result (Amazon Web Services, n.d.).&lt;/p&gt;
&lt;p&gt;The application using the API doesn't need to know all the internal details of the system providing the service. It needs to understand the interface and follow the rules for using it.&lt;/p&gt;
&lt;h4 id=&quot;the-restaurant-menu-analogy&quot;&gt;The restaurant menu analogy&lt;/h4&gt;
&lt;p&gt;Think of a restaurant menu. It tells you what you can order and what to expect back: &quot;Quarter chicken and chips with lettuce&quot;, and sometimes even a picture of the plate you're about to get.&lt;/p&gt;
&lt;p&gt;You don't need to know how the kitchen works, where the ingredients are stored, or what the chef does behind the scenes. You look at the menu and place your order. That's it.&lt;/p&gt;
&lt;p&gt;In this analogy, the &lt;strong&gt;menu is the API&lt;/strong&gt; (the interface), and your &lt;strong&gt;order is the request&lt;/strong&gt;. It's basically: &quot;I want this. I don't need to know how you do it, but here's how I'll ask for it.&quot;&lt;/p&gt;
&lt;p&gt;A quick caveat, though. The menu doesn't cook anything. In a typical web API, the API defines the interaction, while the server behind it does the actual work. Keep that in mind, because we'll come back to it.&lt;/p&gt;
&lt;h4 id=&quot;apis-aren-t-only-for-the-internet&quot;&gt;APIs aren't only for the internet&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; An API isn't necessarily a website, a database, or something that runs over the internet. APIs also exist inside programs. For example, a software library has an API that tells you which functions you can call, and an operating system has APIs that let programs open files or draw things on screen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;how-do-apis-work-the-request-response-cycle&quot;&gt;How Do APIs Work? The Request-Response Cycle&lt;/h3&gt;
&lt;p&gt;Let's put the restaurant to work. You're hungry and you order a burger:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;You place an order (request).&lt;/strong&gt; Your application asks for something, like the current weather in Mutare.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The kitchen gets to work (processing).&lt;/strong&gt; The server receives the request and prepares what was asked for.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your food arrives (response).&lt;/strong&gt; The server sends the result back, and your application displays it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's how the roles line up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The menu&lt;/strong&gt; is the interface and its rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The customer&lt;/strong&gt; is the client (your application).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The kitchen&lt;/strong&gt; is the server that processes the request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The meal&lt;/strong&gt; is the response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a typical web API interaction, the client sends a request to an endpoint (a specific web address) using a defined format and method. The server receives the request, validates it, and performs the required operation, which may involve retrieving data or communicating with another service. It then returns a response, often containing a status code and data formatted as JSON (MDN Web Docs, n.d.).&lt;/p&gt;
&lt;h4 id=&quot;what-happens-when-something-goes-wrong-&quot;&gt;What happens when something goes wrong?&lt;/h4&gt;
&lt;p&gt;Not every order ends with a delicious burger. You might order something that isn't on the menu (an invalid request), the kitchen might run out of ingredients (a server error), or the waiter might never make it to your table (a network problem). If something goes wrong, the server may return an error response instead, and the client should be ready to handle both successes and failures.&lt;/p&gt;
&lt;h3 id=&quot;making-your-first-api-request-in-python&quot;&gt;Making Your First API Request in Python&lt;/h3&gt;
&lt;p&gt;Enough theory. Let's actually use an API!&lt;/p&gt;
&lt;p&gt;We'll use Python and &lt;a rel=&quot;nofollow&quot; href=&quot;https://jsonplaceholder.typicode.com/&quot;&gt;JSONPlaceholder&lt;/a&gt;, a free fake online REST API built for testing and experimentation. It lets us practise a simple request without building our own backend.&lt;/p&gt;
&lt;p&gt;First, install the &lt;code&gt;requests&lt;/code&gt; library:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, the code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import requests

url = &quot;https://jsonplaceholder.typicode.com/posts/1&quot;

try:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    post = response.json()
    print(f&quot;User ID: {post['userId']}&quot;)
    print(f&quot;ID: {post['id']}&quot;)
    print(f&quot;Post Title: {post['title']}&quot;)
    print(f&quot;Post Body: {post['body']}&quot;)
except ValueError:
    print(&quot;The response was not valid JSON.&quot;)
except requests.RequestException as error:
    print(&quot;Request failed:&quot;, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it, we've just made an API request! Here's what's happening, in plain language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requests.get(url, timeout=10)&lt;/code&gt; sends an HTTP &lt;strong&gt;GET&lt;/strong&gt; request, which means &quot;please give me this.&quot; It's us placing our order. The &lt;code&gt;timeout=10&lt;/code&gt; tells Python to stop waiting after 10 seconds, so our program doesn't hang forever if the kitchen has vanished.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.raise_for_status()&lt;/code&gt; checks whether the request succeeded. If the server answers with an error status code (like 404 for &quot;not found&quot; or 500 for &quot;server problem&quot;), it raises an exception instead of letting the program carry on as if nothing happened (Requests, n.d.).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.json()&lt;/code&gt; &lt;strong&gt;parses&lt;/strong&gt; the response body, which means it turns the JSON text into Python data (here, a dictionary) that we can work with. If the body isn't valid JSON, this step fails.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post['userId']&lt;/code&gt;, &lt;code&gt;post['id']&lt;/code&gt;, &lt;code&gt;post['title']&lt;/code&gt; and &lt;code&gt;post['body']&lt;/code&gt; each read one field from that dictionary: who wrote the post, the post's own number, its title and its text. The f-strings add a label in front of each value so the output is easy to read.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;except&lt;/code&gt; blocks are our safety net. One catches invalid JSON, and the other catches request failures such as timeouts, connection problems and bad status codes. Either way, we print a readable message instead of crashing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's what I got when I ran the code:&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=923120795178393657&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The API returns a sample post with a user ID, a post ID, a title and a body, and those are the four values we printed. We don't know how the service stores that data, and we don't need to. We use the interface and work with what comes back.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Run this code yourself and then break it on purpose. Change the URL to &lt;code&gt;/posts/99999&lt;/code&gt; or remove a letter from the address, and watch which error message appears. You'll learn more from the failures than from the success.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;types-of-apis-rest-soap-graphql-and-websockets&quot;&gt;Types of APIs: REST, SOAP, GraphQL and WebSockets&lt;/h3&gt;
&lt;p&gt;Do all APIs work the same way? The short answer is no. Here are four you'll hear about often.&lt;/p&gt;
&lt;h4 id=&quot;rest-apis&quot;&gt;REST APIs&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; is an architectural style commonly used to build web APIs, often over HTTP. Resources like users, products or blog posts are accessed through addresses. Our JSONPlaceholder example was a REST API.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Use it when:&lt;/em&gt; you're building a standard web or mobile backend. It's a sensible starting point for most beginners.&lt;/p&gt;
&lt;h4 id=&quot;soap-apis&quot;&gt;SOAP APIs&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;SOAP&lt;/strong&gt; is a messaging protocol that exchanges structured messages, usually written in XML, under strict rules. It's more rigid than REST and is often found in older or enterprise systems.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Use it when:&lt;/em&gt; you're integrating with a system that already requires it, such as some banking or enterprise services.&lt;/p&gt;
&lt;h4 id=&quot;graphql-apis&quot;&gt;GraphQL APIs&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;GraphQL&lt;/strong&gt; is a query language and runtime. Clients can request the specific fields they need, within the limits of the server's schema and permissions. An app showing a profile page might ask only for a user's name and email instead of their entire record.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Use it when:&lt;/em&gt; different screens or clients need different slices of the same data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt; The official &lt;a rel=&quot;nofollow&quot; href=&quot;https://graphql.org/learn/&quot;&gt;GraphQL introduction&lt;/a&gt; explains queries and schemas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;websocket-apis&quot;&gt;WebSocket APIs&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;WebSockets&lt;/strong&gt; provide a persistent, two-way connection, instead of the usual &quot;ask once, answer once&quot; pattern of HTTP. They can support real-time APIs.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Use it when:&lt;/em&gt; you need constant live updates, such as chat, multiplayer games or live dashboards.&lt;/p&gt;
&lt;h4 id=&quot;these-aren-t-all-the-same-kind-of-thing&quot;&gt;These aren't all the same kind of thing&lt;/h4&gt;
&lt;p&gt;Here's the part that confused me at first: these four aren't competing options from the same category. REST is an architectural style, SOAP is a protocol, GraphQL is a query language and runtime, and WebSocket is a communication protocol. They also aren't mutually exclusive. A single application might use REST for normal data, GraphQL for flexible queries and WebSockets for live notifications.&lt;/p&gt;
&lt;p&gt;There's no single API style that fits every situation. The right choice depends on what you're building.&lt;/p&gt;
&lt;h3 id=&quot;why-apis-matter-and-where-they-can-hurt-&quot;&gt;Why APIs Matter (and Where They Can Hurt)&lt;/h3&gt;
&lt;p&gt;APIs let developers build on existing technology, combining services into applications that would be far more complicated to create alone.&lt;/p&gt;
&lt;p&gt;If you're into AI like I am, this matters even more. Picture an AI-powered application that uses a language model, retrieves information from a database and processes payments, all through separate APIs.&lt;/p&gt;
&lt;p&gt;An AI model and an AI API are not the same thing. The model performs the inference, while an API can provide a defined way for an application to send input to a hosted model and receive its output. This means developers can build AI-powered applications without necessarily hosting the model themselves, although they still need to manage the surrounding application logic, security and usage costs.&lt;/p&gt;
&lt;h4 id=&quot;the-trade-offs-of-depending-on-someone-else-s-api&quot;&gt;The trade-offs of depending on someone else's API&lt;/h4&gt;
&lt;p&gt;Although APIs make integration easier, they also introduce considerations such as authentication, rate limits, security and reliability. Some services require API keys, while others restrict the number of requests a client can make or charge based on usage. Requests can also fail because of network issues or server errors. Developers should protect private credentials, follow the provider's usage requirements, and design applications to handle failures gracefully.&lt;/p&gt;
&lt;p&gt;There's also dependency risk. An external API can change, go offline, impose quotas or start charging for something that used to be free. Your app is only as reliable as the services it leans on.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt; The &lt;a rel=&quot;nofollow&quot; href=&quot;https://owasp.org/www-project-api-security/&quot;&gt;OWASP API Security Project&lt;/a&gt; lists the most common API security risks and how to reduce them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;5-beginner-api-projects-to-practise-with&quot;&gt;5 Beginner API Projects to Practise With&lt;/h3&gt;
&lt;p&gt;Now let's practise. Each project below has a clear finish line and at least one error case to handle. The difficulty ratings are rough guides, not universal truths, so a project may feel easier or harder for you. Always check each provider's current documentation for key, account and usage requirements, since these can change.&lt;/p&gt;
&lt;h4 id=&quot;1-weather-app-beginner-&quot;&gt;1. Weather app (beginner)&lt;/h4&gt;
&lt;p&gt;Build an app that shows the temperature, humidity and current conditions for a place of your choice. Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://open-meteo.com/en/docs&quot;&gt;Open-Meteo&lt;/a&gt;, which offers weather data as JSON and, at the time of writing, doesn't need an API key for its free, non-commercial use.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You'll practise:&lt;/strong&gt; GET requests, query parameters, JSON responses and displaying data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success looks like:&lt;/strong&gt; typing a city's coordinates and seeing its current weather on screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error case to handle:&lt;/strong&gt; the request times out or the service is unreachable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-random-cat-image-generator-beginner-&quot;&gt;2. Random cat image generator (beginner)&lt;/h4&gt;
&lt;p&gt;A tiny app that shows a new cat picture each time someone clicks a button. Simple, fun and surprisingly good for the basics. Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.thecatapi.com/&quot;&gt;The Cat API&lt;/a&gt;, and check its docs for current authentication and usage requirements.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You'll practise:&lt;/strong&gt; making requests, handling responses, displaying images and responding to clicks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success looks like:&lt;/strong&gt; a new cat appears on every click.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error case to handle:&lt;/strong&gt; the image fails to load, so show a friendly message instead of a broken picture.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-book-finder-beginner-&quot;&gt;3. Book finder (beginner)&lt;/h4&gt;
&lt;p&gt;Let users search by title or author and show titles, authors, publication dates and cover images with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/books/docs/v1/using&quot;&gt;Google Books API&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You'll practise:&lt;/strong&gt; search queries, query parameters, looping through multiple results and displaying structured data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success looks like:&lt;/strong&gt; searching &quot;Things Fall Apart&quot; and seeing a list of matching books.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error case to handle:&lt;/strong&gt; a search that returns no results. Show &quot;No books found&quot; rather than a blank page.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-movie-search-app-beginner-to-intermediate-&quot;&gt;4. Movie search app (beginner to intermediate)&lt;/h4&gt;
&lt;p&gt;Build a movie lookup that shows titles, release dates, ratings and posters using &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.themoviedb.org/docs/getting-started&quot;&gt;The Movie Database (TMDB) API&lt;/a&gt;. Check the current requirements for accounts, API keys, attribution and usage before you start.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You'll practise:&lt;/strong&gt; authentication, search queries and displaying data in a user interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success looks like:&lt;/strong&gt; searching for a film and seeing its poster, rating and release date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error case to handle:&lt;/strong&gt; an invalid or missing API key.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;5-ai-chatbot-intermediate-&quot;&gt;5. AI chatbot (intermediate)&lt;/h4&gt;
&lt;p&gt;Build a simple chatbot that sends a user's message to an AI model and displays the reply. Follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.openai.com/docs/quickstart&quot;&gt;OpenAI API quickstart&lt;/a&gt; to make your first request. Hosted model APIs need a key and may charge for usage, so keep an eye on costs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You'll practise:&lt;/strong&gt; POST requests, sending data in a request body, authentication and handling AI-generated responses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success looks like:&lt;/strong&gt; typing a question and getting a model-generated answer back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error case to handle:&lt;/strong&gt; the request fails, the rate limit is hit or the key is invalid.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Never put private API keys in frontend code or commit them to GitHub. For a web app, make the API call from your server and load the key from an environment variable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;where-should-you-start-&quot;&gt;Where Should You Start?&lt;/h3&gt;
&lt;p&gt;If you've never worked with APIs before, start with the cat generator or the weather app. Both let you focus on the fundamentals without much application logic getting in the way.&lt;/p&gt;
&lt;p&gt;Once you're comfortable, move on to the book finder and movie search. Save the chatbot for when you're ready to deal with authentication, POST requests and keeping secrets safe.&lt;/p&gt;
&lt;p&gt;Start small, experiment, break things, and build your way up. The goal isn't only to finish the projects. It's to understand what happens each time your application talks to another service.&lt;/p&gt;
&lt;h3 id=&quot;frequently-asked-questions-1&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;do-i-need-to-know-how-a-server-works-to-use-an-api-&quot;&gt;Do I need to know how a server works to use an API?&lt;/h4&gt;
&lt;p&gt;No. That's the whole point of an interface. You need to understand how to ask and what you'll get back, not how the system works internally.&lt;/p&gt;
&lt;h4 id=&quot;what-s-the-difference-between-an-api-and-an-api-key-&quot;&gt;What's the difference between an API and an API key?&lt;/h4&gt;
&lt;p&gt;The API is the interface. The API key is like a membership card that some providers require so they know who is making requests and can apply limits or charges.&lt;/p&gt;
&lt;h4 id=&quot;is-a-free-api-really-free-&quot;&gt;Is a free API really free?&lt;/h4&gt;
&lt;p&gt;Often, but usually with limits. Check the provider's documentation for rate limits, usage rules and whether free access is allowed for your kind of project.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-7&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;An &lt;strong&gt;API&lt;/strong&gt; is a defined interface that lets software components interact without needing to know each other's inner workings, much like ordering from a menu without visiting the kitchen.&lt;/p&gt;
&lt;p&gt;In a typical web API, the &lt;strong&gt;client&lt;/strong&gt; sends a &lt;strong&gt;request&lt;/strong&gt;, the &lt;strong&gt;server&lt;/strong&gt; processes it, and a &lt;strong&gt;response&lt;/strong&gt; comes back, which might be data or an error.&lt;/p&gt;
&lt;p&gt;We saw this in Python with &lt;code&gt;requests&lt;/code&gt;, adding a &lt;strong&gt;timeout&lt;/strong&gt;, &lt;code&gt;raise_for_status()&lt;/code&gt; and &lt;strong&gt;exception handling&lt;/strong&gt; so the code copes with failure.&lt;/p&gt;
&lt;p&gt;REST, SOAP, GraphQL and WebSockets are different tools that can even live in the same app.&lt;/p&gt;
&lt;p&gt;APIs make integration possible, but they bring &lt;strong&gt;authentication&lt;/strong&gt;, &lt;strong&gt;rate limits&lt;/strong&gt;, &lt;strong&gt;costs&lt;/strong&gt; and &lt;strong&gt;dependency risks&lt;/strong&gt;. Start with a small project, break it on purpose, and learn from what happens.&lt;/p&gt;
&lt;p&gt;Now that you know what's going on behind the scenes, the next time you use an app, you might catch yourself wondering which APIs are making it work.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;See you next time!&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;p&gt;Amazon Web Services. (n.d.). &lt;em&gt;What is an API?&lt;/em&gt; Amazon.com. Retrieved October 14, 2026, from &lt;a rel=&quot;nofollow&quot; href=&quot;https://aws.amazon.com/what-is/api/&quot;&gt;https://aws.amazon.com/what-is/api/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MDN Web Docs. (n.d.). &lt;em&gt;HTTP&lt;/em&gt;. Mozilla. Retrieved October 14, 2026, from &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Requests. (n.d.). &lt;em&gt;Quickstart&lt;/em&gt;. Requests documentation. Retrieved October 14, 2026, from &lt;a rel=&quot;nofollow&quot; href=&quot;https://requests.readthedocs.io/en/latest/user/quickstart/&quot;&gt;https://requests.readthedocs.io/en/latest/user/quickstart/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31206/what-are-apis-and-how-do-they-work</guid>
<pubDate>Thu, 15 Oct 2026 13:41:10 +0000</pubDate>
</item>
</channel>
</rss>