DEV Community

Cover image for If Your Script Needs 20 Packages to Write a CSV Header, Buyers Can Smell It"**

If Your Script Needs 20 Packages to Write a CSV Header, Buyers Can Smell It"**

I once lost a whole evening to a CSV header. I had 40 assets ready to upload to Adobe Stock, ran my batch script, and every single row got rejected on import. The error was vague, something like "invalid file format". I opened my script and counted the imports at the top: pandas, requests, rich, click, python-dotenv, tqdm, and a few more I'd added "just in case". Twenty packages to write a row of comma-separated text. Somewhere in one of those dependency trees, a default had changed and my output had a BOM and the wrong column count. Adobe's uploader checks the header before it checks anything else. Wrong header, instant rejection, no partial credit.

That night I rewrote the metadata script with zero imports beyond the standard library. It has worked on every run since. Here is what I learned about why bloated scripts and rejected uploads are the same problem.

The platforms check your header first, and they mean it

Adobe Stock wants a 5-column CSV. Vecteezy wants 4 columns. Dreamstime wants 15. These are not suggestions. The upload parsers reject a file with the wrong header row before they look at a single image. If you build your CSV with pandas, you get whatever quoting, encoding, and index behavior pandas defaults to this version, and those defaults change between releases. I have seen uploads fail because a library update silently added an index column.

The fix is boring: write the header yourself, as a literal string, and append rows with csv.writer from the standard library. My make_metadata.py does exactly that. It takes a folder of files, title-cases the filenames if you have not supplied titles, and writes the exact column layout each platform demands. No install step at all. Two third-party packages in my whole toolkit, Pillow for covers and reportlab for PDFs, and neither one touches the metadata path.

Double-posting is the fastest way to get flagged

This one cost me real anxiety early on. You run a batch of 58 uploads, your terminal glitches, you are not sure which ones went through, so you run it again. Now you have duplicates on a brand new contributor account. Reviewers notice, and duplicate submissions are one of the listed reasons accounts get rejected during review.

The fix is a ledger, not a database. My upload_tracker.py is a plain text file with one asset name per line, plus three commands: pending, done, and status. Standard library only. When your batch script starts an upload, it checks the ledger first. When it finishes, it appends. A re-run becomes mathematically unable to double-post. It took maybe 80 lines of Python and it has saved me from myself more times than I can count.

Your server is smaller than you think, and that is fine

The whole pipeline ran on 1 vCPU and 2 GB RAM, no GPU. That is roughly the cheapest VPS tier every provider sells. It works if you respect the box:

  • One process at a time. Never fan out image jobs in parallel on a single core, you just thrash.
  • Call gc.collect() after each asset. Python holds references longer than you expect.
  • Measure peak RSS, not average. resource.getrusage(resource.RUSAGE_SELF).ru_maxrss gives you the real ceiling. My average usage looked fine; the peak was what killed runs.
  • For heavy render steps, spawn one file per subprocess so memory actually returns to the OS when it exits.
  • Inspect previews at 60 dpi, render finals at 300. I wasted weeks rendering full resolution just to check layout.

If your script needs 20 packages, you cannot reason about any of this. You do not know which dependency is eating the RAM. With two packages, you know exactly where the memory goes.

Packaging is what separates a file from a product

I sold my first asset on Adobe for $0.99 and it felt the same as any sale since. But the first thing I ever sold directly was a zip with a real cover image, and the difference in how people responded was immediate. A bare file looks like a dump. A zip with a 1280×720 cover and a 600×600 thumbnail looks like something someone finished.

pack_product.py does this in one command: zips the folder, renders the cover and thumbnail with Pillow, drops everything in a _system folder you can relocate with --dir or an environment variable. No editing the script, no config file. Same philosophy: one job, standard library plus one dependency, done.

The folder structure matters more than the framework

Every pipeline I tried before this one died as a junk drawer. The layout that stuck:

project/
  assets/       raw output
  output/       ready-to-submit files + metadata
  packs/        finished zips + covers
  research/     notes, scrape logs, sources
  scripts/      the tools
Enter fullscreen mode Exit fullscreen mode

One rule I learned the hard way: keep research/ forever. I deleted mine once to "clean up" and lost the raw material for three products I later wanted to make. Notes and logs are inventory, not clutter.

Where this came from

These scripts are not a theory. They packaged 39 products, 856 MB of output, sitting under roughly 19,000 lines of pipeline code and 300+ pages of research into the platform rules they enforce. 1,700 assets made, 58 live on Adobe, first sale $0.99. All of it on that 1 vCPU, 2 GB box.

I put the four scripts together as the digital asset factory, the packaging layer of that system, with the exact commands and folder layout above. If you are stalling on metadata, double-posts, or packaging, this is the unglamorous layer that unstuck me.

Get digital asset factory


Building digital products? Free guides & kits for digital product sellers.

Top comments (0)