Automate SFTP and FTP file drops with Python.
A partner drops a file every night. A qbash Task checks the folder on a schedule, loads each new file into your database and posts a summary to Slack.
The task.
A partner_file_loads table records every file already loaded, by name and modified time, so a reused file name still loads. Each new CSV streams into Postgres 1,000 rows at a time. A file that fails is reported in the Slack summary and tried again on the next run, and the other files still load.
folder = "/outbound" # A file counts as loaded by name and modified time, so a partner that # reuses a file name still gets each new delivery loaded. loaded = { (row["file_name"], row["file_mtime"]) for row in qbash.integrations.postgres.execute_query( query="SELECT file_name, file_mtime FROM partner_file_loads", )["result"] } entries = qbash.integrations.sftp.list_files(path=folder)["entries"] new_files = [ e for e in entries if e["type"] == "file" and e["name"].endswith(".csv") and (e["name"], e["mtime"]) not in loaded ] def load_file(name): path = f"{folder}/{name}" # Counting first also indexes the file, so each SFTP read seeks to its chunk. total = qbash.integrations.sftp.get_row_count(path=path)["row_count"] for start in range(0, total, 1000): rows = qbash.integrations.sftp.read_chunk( path=path, startRow=start, rowCount=1000, )["rows"] qbash.integrations.postgres.execute_query( query=( "INSERT INTO partner_orders (order_id, sku, quantity) " "SELECT order_id, sku, quantity FROM jsonb_to_recordset($1::jsonb) " "AS r(order_id text, sku text, quantity int) " "ON CONFLICT (order_id) DO NOTHING" ), query_params=[rows], ) return total loaded_now, failed = [], [] for entry in new_files: name = entry["name"] try: row_count = load_file(name) except Exception as error: failed.append({"file": name, "error": str(error)}) continue qbash.integrations.postgres.execute_query( query=( "INSERT INTO partner_file_loads (file_name, file_mtime, row_count) " "VALUES ($1, $2, $3)" ), query_params=[name, entry["mtime"], row_count], ) loaded_now.append(name) if loaded_now or failed: qbash.integrations.slack.create_message( channel="#partner-data", text=f"Loaded {len(loaded_now)} new partner files from {folder}. {len(failed)} failed.", ) qbash.output({"files_loaded": loaded_now, "files_failed": failed})
qbash Tasks.
A Task is an AI automation written in Python, hosted on a managed runtime. A developer writes the script; anyone runs it from a form. Integrations, model calls, version control, and logging are built in.
Model and provider agnostic.
Anthropic
OpenAI
Gemini
xAI
OpenRouterAmazon
PerplexityMoonshotAI
MetaQwen
What runs around the code.
Put the Task on a schedule and qbash runs it, holds the server credentials and keeps a record of every run.
- Run it hourly, nightly or on any other cron schedule.
- One connector covers SFTP, FTP and FTPS. Set the protocol and port when you connect the server.
- Each run keeps its output and log, so you can see which files loaded and which failed, and why.
Replace the script on the server.
Move an existing transfer script across, or add a model step for files that are not tidy CSVs.
Replacing a paramiko cron script
The usual setup is a script on a server, a cron keeping it on schedule and credentials in environment variables. When the connection drops on file 143 of 200, there is no checkpoint, so tomorrow starts at file 1. A partner key rotation shows up as missing Monday data. In a Task the processing code stays, and the server, the crontab and the environment variables go.
import paramiko import os ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect( os.environ["SFTP_HOST"], username=os.environ["SFTP_USER"], password=os.environ["SFTP_PASS"], ) sftp = ssh.open_sftp() for filename in sftp.listdir("/incoming"): sftp.get(f"/incoming/{filename}", f"/tmp/{filename}") process_file(f"/tmp/{filename}")
Structured extraction from each file
For files that are not tidy CSVs, run a model over each one to classify it, extract structured fields or write a summary. output_schema constrains the response to your JSON shape, so downstream code reads typed fields instead of parsing free text.
invoice = qbash.ai.raw( instruction=f"Extract the invoice fields. {file_content}", output_schema={ "type": "object", "properties": { "vendor": {"type": "string"}, "amount": {"type": "number"}, "due_date": {"type": "string"}, }, }, )
Where qbash fits next to transfer software.
qbash runs the step after the transfer: reading each file, loading the rows and reporting on the result.
| Desktop transfer tools and MFT suites | qbash | |
|---|---|---|
| Moving files on a schedule | Yes | Yes |
| Reading and reshaping the data | A separate script | In the same Task |
| Loading into a database or spreadsheet | A separate script | Built-in connectors |
| Reports and alerts | Email on success or failure | A summary in Slack, email or a file |
| Where it runs | A Windows server or a managed MFT service | Hosted on qbash |
Questions
Which tables does the example need?
Two tables in your Postgres database. partner_orders holds the rows, with order_id text as its primary key plus sku text and quantity int. partner_file_loads records each loaded file, with file_name text, file_mtime text and row_count int, and a unique constraint on (file_name, file_mtime). Change the columns to match your files.
Does qbash support FTP as well as SFTP?
Yes. The SFTP/FTP integration supports SFTP, FTP and FTPS, and the same methods (list_files, read_chunk, download_file) work for each.
Can a Task start when a new file arrives?
A Task starts on a schedule and checks the folder each time it runs. Choose an interval that matches how often the partner delivers.
How large a file can the task handle?
read_chunk reads a CSV 1,000 rows at a time, so a file never has to fit in memory. On SFTP, get_row_count indexes the file first, so each read seeks straight to its chunk. Plain FTP has no index and rereads the file up to each chunk, so for very large FTP files use download_file instead. If a run stops partway through a file, the next run reads that file again from the start, and ON CONFLICT skips the rows already loaded.
What if a file is still uploading when the Task runs?
Ask the partner to upload under a temporary name, such as orders.csv.part, and rename the file when the upload finishes. The Task only picks up names that end in .csv.
How do I rotate SFTP credentials?
Connect the server once at the org level. Rotating the key is one credential update in qbash, and every Task that uses the connection picks it up without a redeploy.
Can I test the SFTP logic without hitting a real server?
Yes. testing.mock_sftp() provides a mock SFTP client for tests.py, so you can assert that the processing logic works without making a real connection.
Is this an enterprise MFT platform?
No. If you need HIPAA or GLBA compliance, partner onboarding portals and multi-protocol EDI orchestration, Kiteworks, GoAnywhere or Couchdrop are built for that. qbash is for developers automating partner file drops in Python.
Competitor details reviewed . Vendors change plans and features without notice, so check theirs before deciding.
Related
Ship your first task.
Connect your SFTP or FTP server, write the loading script and put it on a schedule.