<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tsvetan Gerginov</title>
    <description>The latest articles on DEV Community by Tsvetan Gerginov (@tsvetang2).</description>
    <link>https://dev.to/tsvetang2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981886%2F690b2298-4fe0-4369-b618-b3fbdda1e0e5.jpeg</url>
      <title>DEV Community: Tsvetan Gerginov</title>
      <link>https://dev.to/tsvetang2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tsvetang2"/>
    <language>en</language>
    <item>
      <title>Building a Resilient Instagram Scraper With Selenium — What Mimicking Human Behavior Actually Looks Like</title>
      <dc:creator>Tsvetan Gerginov</dc:creator>
      <pubDate>Fri, 12 Jun 2026 20:54:27 +0000</pubDate>
      <link>https://dev.to/tsvetang2/building-a-resilient-instagram-scraper-with-selenium-what-mimicking-human-behavior-actually-looks-egc</link>
      <guid>https://dev.to/tsvetang2/building-a-resilient-instagram-scraper-with-selenium-what-mimicking-human-behavior-actually-looks-egc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Up front:&lt;/strong&gt; this is a personal/research tool for downloading from &lt;strong&gt;public&lt;/strong&gt; Instagram profiles. Use it responsibly and within &lt;a href="https://help.instagram.com/581066165581870" rel="noopener noreferrer"&gt;Instagram's Terms of Service&lt;/a&gt; and your local laws. This post is about the engineering — specifically, what it takes to make browser automation behave less like a bot — not about evading anyone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scraping any modern social platform is less a parsing problem and more a &lt;em&gt;behavioral&lt;/em&gt; one. The HTML is the easy part. The hard part is that the site is actively watching how you act, and the moment you act like a script — instant scroll to the bottom, requests at machine speed, no pauses — you hit a challenge page and you're done.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/TsvetanG2/InstagramWrapperPostScraper" rel="noopener noreferrer"&gt;InstagramWrapperPostScraper&lt;/a&gt;&lt;/strong&gt; as a Python + Selenium tool that drives a real Microsoft Edge browser to download photos, videos, and captions from public profiles. The interesting engineering isn't "how do I find the image URL" — it's "how do I make a browser automation script move through a page the way a person would." MIT licensed, Python 3.10+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a real browser instead of an API or HTTP
&lt;/h2&gt;

&lt;p&gt;There are three broad ways to pull data off Instagram, and they fail differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API approaches&lt;/strong&gt; run into rate limits fast and require credentials/tokens that get throttled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain HTTP scraping&lt;/strong&gt; is brittle and trivially detectable — no JS execution, obvious request patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driving a real browser&lt;/strong&gt; (this approach) executes the actual page JS, renders like a human's session, and can keep working through temporary rate-limit blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: a real browser is slower and heavier. But for a personal-scale download tool, reliability beats speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actually-interesting part: human-like behavior
&lt;/h2&gt;

&lt;p&gt;The most recent version (0.0.2) is almost entirely about making the scroll behavior look human, and this is the part I'd point any automation person to. A naive scraper does &lt;code&gt;scrollTo(bottom)&lt;/code&gt; and fires requests as fast as the network allows. This one deliberately doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Randomized scroll steps&lt;/strong&gt; — it scrolls 50–90% of the viewport at a time, not straight to the bottom&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Occasional scroll-ups&lt;/strong&gt; — sometimes it scrolls back up, the way a human re-reads something&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random pauses&lt;/strong&gt; — 2–5 seconds between actions instead of hammering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longer initial waits&lt;/strong&gt; — 4–7 seconds when first opening a profile (bumped up from 3–5s)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Periodic challenge checks&lt;/strong&gt; — every 10 scrolls it checks whether a rate-limit/challenge page has appeared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point connects to the other 0.0.2 improvement: a dedicated &lt;code&gt;_is_challenge_page()&lt;/code&gt; method that recognizes captcha/challenge pages by checking the &lt;strong&gt;URL plus DOM selectors&lt;/strong&gt;, rather than naively grepping the page source. Source-string matching gives false positives the moment Instagram tweaks copy; checking structure is more robust.&lt;/p&gt;

&lt;p&gt;There's also better end-of-profile detection — it retries scroll up/down 5 times before concluding it's actually reached the bottom, instead of giving up after one attempt — and a carousel retry path that handles duplicate slide URLs and skips blocked slides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean output structure
&lt;/h2&gt;

&lt;p&gt;One thing I cared about: the downloads should be &lt;em&gt;usable&lt;/em&gt;, not a flat dump of files. Each post gets its own folder, carousels keep their slide order, and every post's caption is saved alongside the media:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;downloads/
└── username/
    ├── images/
    │   ├── post_1/
    │   │   ├── username_1.jpg
    │   │   └── description.txt
    │   └── post_2/
    │       ├── username_2_01.jpg   ← carousel slide 1
    │       ├── username_2_02.jpg   ← carousel slide 2
    │       └── description.txt
    └── videos/
        └── post_3/
            ├── username_3.mp4
            └── description.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;I'd rather you know the walls before you hit them. Straight from the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public profiles only&lt;/strong&gt; — private profiles need the scraper account to follow them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge only&lt;/strong&gt; — no Chrome or Firefox support; it relies on Edge WebDriver&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instagram UI changes break selectors&lt;/strong&gt; — when that happens, update Selenium/Edge and retry. This is the permanent tax on scraping anything you don't control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits still apply&lt;/strong&gt; — on very large profiles (1000+ posts) expect pauses and retries; the human-like behavior reduces blocks, it doesn't make you invincible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No proxy support&lt;/strong&gt; — every request comes from your real IP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last two are deliberately on the label. This isn't a tool that pretends to be undetectable, and I'd be suspicious of any that did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway worth stealing
&lt;/h2&gt;

&lt;p&gt;Even if you never touch Instagram, the general lesson ports to any Selenium/Playwright automation: &lt;strong&gt;the gap between "works once on my machine" and "works repeatedly" is almost entirely about timing and behavioral realism.&lt;/strong&gt; Randomized waits, partial scrolls, structural (not string-based) state detection, and retry-with-backoff are the difference between a script that runs and a script that keeps running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 Repo: &lt;a href="https://github.com/TsvetanG2/InstagramWrapperPostScraper" rel="noopener noreferrer"&gt;github.com/TsvetanG2/InstagramWrapperPostScraper&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built browser automation that has to survive a hostile, frequently-changing site, I'd like to hear which behavioral tricks actually moved the needle for you.&lt;/p&gt;

</description>
      <category>python</category>
      <category>selenium</category>
      <category>automation</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>I Built an MCP Server With 132 Tools So Claude Can Manage Cognigy.AI Agents for Me</title>
      <dc:creator>Tsvetan Gerginov</dc:creator>
      <pubDate>Fri, 12 Jun 2026 20:48:31 +0000</pubDate>
      <link>https://dev.to/tsvetang2/i-built-an-mcp-server-with-132-tools-so-claude-can-manage-cognigyai-agents-for-me-2dd0</link>
      <guid>https://dev.to/tsvetang2/i-built-an-mcp-server-with-132-tools-so-claude-can-manage-cognigyai-agents-for-me-2dd0</guid>
      <description>&lt;p&gt;I've spent some quite of time building conversational AI agents on &lt;a href="https://www.cognigy.com/" rel="noopener noreferrer"&gt;Cognigy.AI&lt;/a&gt; — enterprise voice bots, multilingual flows, NLU training, the works while working at Deloitte. It's a powerful platform. It's also a &lt;em&gt;lot&lt;/em&gt; of clicking. Create flow, open node editor, configure node, train intents, create snapshot, promote to next environment... and now we live in a world where my coding assistant can write entire applications, but couldn't touch any of that.&lt;/p&gt;

&lt;p&gt;So I fixed it. &lt;strong&gt;&lt;a href="https://github.com/TsvetanG2/cognigy-ai-mcp-management-server" rel="noopener noreferrer"&gt;cognigy-ai-mcp-management-server&lt;/a&gt;&lt;/strong&gt; is a local MCP (Model Context Protocol) server that gives AI assistants like Claude, Claude Code, and Cursor programmatic access to the Cognigy.AI Management API. &lt;strong&gt;132 tools&lt;/strong&gt;, TypeScript, MIT licensed, &lt;a href="https://www.npmjs.com/package/cognigy-ai-mcp-management-server" rel="noopener noreferrer"&gt;on npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of clicking through the UI, you can now tell your assistant things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Create a new intent for order cancellation with these example sentences, then retrain the NLU"&lt;/li&gt;
&lt;li&gt;"Run the regression test playbook and summarize what broke"&lt;/li&gt;
&lt;li&gt;"Diff the current snapshot against production and tell me what changed"&lt;/li&gt;
&lt;li&gt;"Find every flow that uses this deprecated connection"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;The 132 tools span essentially the whole management surface: flows and nodes (full CRUD plus search and AI output generation), intents and NLU training, playbooks and regression testing, snapshots and packages (create, diff, promote across environments), Knowledge AI / RAG stores (21 tools just for that), LLM provider configuration, connections, extensions, contact profiles with GDPR export, analytics, and audit logs. The full list lives in &lt;a href="https://github.com/TsvetanG2/cognigy-ai-mcp-management-server/blob/master/TOOLS.md" rel="noopener noreferrer"&gt;TOOLS.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions I'd defend in a code review
&lt;/h2&gt;

&lt;p&gt;Building an MCP server that can &lt;em&gt;mutate production conversational AI agents&lt;/em&gt; forces you to think about safety differently than a read-only integration. A few choices I made:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;dryRun: true&lt;/code&gt; by default on every mutating tool.&lt;/strong&gt; An LLM with write access to your production agents is a chainsaw. Every tool that creates, updates, or deletes anything defaults to a dry run — the assistant sees exactly what &lt;em&gt;would&lt;/em&gt; happen and has to explicitly flip the flag to execute. The destructive path requires intent, not just a hallucinated tool call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Secrets never reach the model.&lt;/strong&gt; API keys live only in environment variables and memory; connection secrets coming back from the Cognigy API are automatically redacted before the LLM sees them. Your model context should never contain credentials — that's non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Async-aware tooling.&lt;/strong&gt; NLU training and snapshot operations are long-running tasks on Cognigy's side. The tools poll task status until completion instead of returning a job ID and leaving the LLM to guess, which keeps multi-step agent workflows from silently desyncing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Zod validation on every input.&lt;/strong&gt; LLMs produce &lt;em&gt;mostly&lt;/em&gt; correct tool arguments. "Mostly" is doing heavy lifting in that sentence. Every tool validates its inputs with Zod schemas before anything hits the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Peer dependency instead of bundling.&lt;/strong&gt; Cognigy's official REST client is published under a proprietary license, so this server declares it as a peer dependency rather than bundling it. You install it yourself and accept Cognigy's terms directly; my MIT license covers only my code. Licensing hygiene is boring until it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mock-first development
&lt;/h2&gt;

&lt;p&gt;You don't need a Cognigy account to hack on this. The repo ships with a Prism mock server generated from the OpenAPI spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1&lt;/span&gt;
npm run mock

&lt;span class="c"&gt;# Terminal 2&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt;   &lt;span class="c"&gt;# 49 tests against the mock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript types are also generated from the OpenAPI spec (&lt;code&gt;npm run gen:types&lt;/code&gt;), so when Cognigy updates their API, regenerating types surfaces breakage at compile time instead of at runtime in someone's production tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @cognigy/rest-api-client   &lt;span class="c"&gt;# official Cognigy SDK, their license&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; cognigy-ai-mcp-management-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your MCP client at it — for Claude Code, drop this in &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cognigy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cognigy-ai-mcp-management-server"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"COGNIGY_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api-trial.cognigy.ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"COGNIGY_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-api-key-here"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with a free Cognigy trial account, SaaS, or on-prem. Configs for Claude Desktop and Cursor are in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest footnote
&lt;/h2&gt;

&lt;p&gt;Mid-project, I discovered that NiCE (Cognigy's parent company) had shipped an official MCP server. Did I consider abandoning mine? For about an hour. Then I kept going, because (a) I was learning an enormous amount about the Management API surface by mapping all of it, and (b) an independent, MIT-licensed, mock-testable implementation with dryRun-by-default semantics is a different artifact than an official one. If the official server fits your needs better — use it! This one exists, it's open, and the code shows exactly how to wrap a large enterprise API into LLM-safe tooling. That alone made it worth shipping.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Standard disclaimer: this is an independent project, not affiliated with or endorsed by Cognigy or NiCE. You need your own Cognigy account and API key.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you build on Cognigy.AI — as a developer, solution architect, or SI partner — I'd genuinely love to hear what workflows you'd automate first. And if you're building MCP servers for &lt;em&gt;other&lt;/em&gt; enterprise platforms, the dryRun + redaction + async-polling patterns here are portable; steal them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cognigy</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Got Tired of Downloading Email Attachments One by One, So I Built a Desktop App for It</title>
      <dc:creator>Tsvetan Gerginov</dc:creator>
      <pubDate>Fri, 12 Jun 2026 20:42:41 +0000</pubDate>
      <link>https://dev.to/tsvetang2/i-got-tired-of-downloading-email-attachments-one-by-one-so-i-built-a-desktop-app-for-it-cl</link>
      <guid>https://dev.to/tsvetang2/i-got-tired-of-downloading-email-attachments-one-by-one-so-i-built-a-desktop-app-for-it-cl</guid>
      <description>&lt;p&gt;Picture this: 200 invoices in your inbox, scattered across six months of emails, and accounting needs all of them in a folder. Today. Gmail's UI gives you exactly one way to do this — open email, click attachment, download, repeat. Two hundred times.&lt;/p&gt;

&lt;p&gt;I refused. So I built &lt;strong&gt;&lt;a href="https://github.com/TsvetanG2/Email-Attachment-Downloader" rel="noopener noreferrer"&gt;Email Attachment Downloader&lt;/a&gt;&lt;/strong&gt; — an open-source desktop app that bulk-downloads attachments from Gmail and Outlook with filtering, auto-renaming, and a modern dark GUI. Python, MIT licensed, runs on Windows, macOS, and Linux.&lt;/p&gt;

&lt;h1&gt;
  
  
  What it does
&lt;/h1&gt;

&lt;p&gt;The workflow is simple: connect to your inbox over IMAP, filter, preview, download. But the details are where it earns its keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filter by sender, subject, and date range&lt;/strong&gt; — &lt;code&gt;invoices@company.com&lt;/code&gt; + "invoice" + Jan–Mar gets you exactly the emails you need, with a built-in calendar picker for dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File type selection&lt;/strong&gt; — download only PDFs, or only spreadsheets, or images, documents, presentations, archives — your call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview before download&lt;/strong&gt; — see every matching email and its attachments, deselect the noise, then pull the trigger&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-renaming patterns&lt;/strong&gt; — this is my favorite part. &lt;code&gt;invoice.pdf&lt;/code&gt; becomes &lt;code&gt;2024-01-15_invoice.pdf&lt;/code&gt; or &lt;code&gt;john_2024-01-15_invoice.pdf&lt;/code&gt; automatically. Anyone who's ended up with &lt;code&gt;invoice.pdf&lt;/code&gt;, &lt;code&gt;invoice(1).pdf&lt;/code&gt;, &lt;code&gt;invoice(14).pdf&lt;/code&gt; knows the pain this solves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threaded downloads&lt;/strong&gt; — attachments download in parallel without freezing the UI, with a real-time progress bar and activity log&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.10+&lt;/strong&gt; with plain &lt;strong&gt;IMAP&lt;/strong&gt; (&lt;code&gt;imaplib&lt;/code&gt;) for email access — no Gmail API credentials, no OAuth app registration, no Google Cloud Console. An App Password and you're in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/TomSchimansky/CustomTkinter" rel="noopener noreferrer"&gt;CustomTkinter&lt;/a&gt;&lt;/strong&gt; for the GUI — if you think Tkinter apps have to look like Windows 95, CustomTkinter will change your mind. Modern dark theme, clean widgets, zero web stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/j4321/tkcalendar" rel="noopener noreferrer"&gt;tkcalendar&lt;/a&gt;&lt;/strong&gt; for the date picker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is deliberately modular — &lt;code&gt;email_client.py&lt;/code&gt; handles IMAP, &lt;code&gt;downloader.py&lt;/code&gt; handles extraction and saving, &lt;code&gt;renamer.py&lt;/code&gt; is pure renaming logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security — because it's your inbox
&lt;/h2&gt;

&lt;p&gt;An app that asks for your email password deserves scrutiny, so here's the deal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your password is &lt;strong&gt;never stored&lt;/strong&gt; — it lives in memory for the session only&lt;/li&gt;
&lt;li&gt;It works with &lt;strong&gt;App Passwords&lt;/strong&gt; (recommended), so your real password never touches the app&lt;/li&gt;
&lt;li&gt;All connections use &lt;strong&gt;SSL/TLS&lt;/strong&gt; (IMAP over port 993)&lt;/li&gt;
&lt;li&gt;The app is &lt;strong&gt;read-only&lt;/strong&gt; — it never modifies or deletes emails&lt;/li&gt;
&lt;li&gt;And it's open source, so you don't have to take my word for any of this — read the code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README has step-by-step App Password setup guides for both Gmail and Outlook, because that part trips up everyone the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;It searches your INBOX folder over IMAP — if your invoices live in a label/subfolder, or your provider doesn't do IMAP, you're out of scope for now. And it's a desktop tool by design: no cloud, no web version, your credentials and files stay on your machine. I consider that last one a feature, but your mileage may vary.&lt;/p&gt;

&lt;p&gt;There's a &lt;a href="https://github.com/TsvetanG2/Email-Attachment-Downloader/releases" rel="noopener noreferrer"&gt;Windows installer&lt;/a&gt; if you just want to use it, or clone and &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; if you want to poke at the code. Issues and PRs welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/TsvetanG2/Email-Attachment-Downloader" rel="noopener noreferrer"&gt;github.com/TsvetanG2/Email-Attachment-Downloader&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Installer: &lt;a href="https://github.com/TsvetanG2/Email-Attachment-Downloader/releases" rel="noopener noreferrer"&gt;Releases page&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the most attachments you've ever had to download by hand? I'll start: enough to build this app.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>I've built a open source PDF-To-Excel-Converter</title>
      <dc:creator>Tsvetan Gerginov</dc:creator>
      <pubDate>Fri, 12 Jun 2026 20:31:49 +0000</pubDate>
      <link>https://dev.to/tsvetang2/ive-built-a-open-source-pdf-to-excel-converter-5den</link>
      <guid>https://dev.to/tsvetang2/ive-built-a-open-source-pdf-to-excel-converter-5den</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/TsvetanG2/PDF-To-Excel-Converter" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hi community,&lt;/p&gt;

&lt;p&gt;I've built a open source PDF to Excel Converter and let me tell you why!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We've all been there:&lt;/strong&gt; someone sends you a 40-page PDF report and asks for "the numbers in a spreadsheet by Friday." You can copy-paste cell by cell, pay for a SaaS converter that uploads your (possibly confidential) data to who-knows-where, or... build your own tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The core idea is simple: upload a PDF, pick an extraction mode, download an &lt;code&gt;.xlsx&lt;/code&gt;. The interesting part is in the modes, because "convert a PDF" means different things depending on the document:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. All Text + Tables&lt;/strong&gt; — extracts everything (paragraphs, headings, tables) and consolidates it into a single worksheet. Useful when you need the full content of a document in a structured, searchable format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tables Only&lt;/strong&gt; — ignores the prose and hunts down tabular data specifically. Each detected table lands in its own sheet in the workbook. This is the mode you want for financial reports, invoices, or anything where the tables &lt;em&gt;are&lt;/em&gt; the data.&lt;/p&gt;

&lt;p&gt;That second mode is where most converters fall short — they either flatten tables into mush or miss them entirely. Splitting each table into a separate sheet keeps the structure intact and makes downstream work (pivot tables, formulas, imports) actually possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Nothing exotic, and that's deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python + Flask&lt;/strong&gt; for the web app — file upload, mode selection, conversion, download. One form, one job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jsvine/pdfplumber" rel="noopener noreferrer"&gt;pdfplumber&lt;/a&gt;&lt;/strong&gt; for text and layout-aware extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/chezou/tabula-py" rel="noopener noreferrer"&gt;tabula-py&lt;/a&gt;&lt;/strong&gt; for table detection and extraction&lt;/li&gt;
&lt;li&gt;A separate &lt;strong&gt;desktop version&lt;/strong&gt; in the repo for people who don't want to run a server at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why two extraction libraries? Because PDFs are chaos. A PDF is fundamentally a &lt;em&gt;visual&lt;/em&gt; format — it knows where to draw characters, not what a "table" is. pdfplumber is excellent at layout-aware text extraction, while tabula's table detection handles structured grids better. Using each for what it does best gives much more reliable output than forcing one library to do everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local-first matters
&lt;/h2&gt;

&lt;p&gt;Most "free PDF converter" sites are upload services. That's fine for a recipe PDF — less fine for contracts, bank statements, or client data. This tool processes everything locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/TsvetanG2/PDF-To-Excel-Converter.git
&lt;span class="nb"&gt;cd &lt;/span&gt;pdf-to-excel-converter
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python pdftoexcel.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://localhost:5000&lt;/code&gt;, upload, convert, done. Your files never leave your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this beats commercial tools on every PDF. Scanned documents (images of text) need OCR, which isn't in scope here — this works on PDFs with an actual text layer. And table detection on documents with creative, merged-cell layouts is a hard problem for &lt;em&gt;every&lt;/em&gt; tool in this space, including this one. For typical reports, exports, and structured documents, it does the job well.&lt;/p&gt;

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