·
8 min read
Write to Fresh Jots from Python — `requests`, `httpx`, or a small SDK
Write to Fresh Jots from Python — `requests`, `httpx`, or a small SDK
A spin-off from Write a note from any project, focused on Python. Three flavors:
- **`pip install freshjots`** — the official zero-dependency Python client (stdlib-only). The recommended path.
- **`requests`** — the bare-HTTP version when you don't want any new dependency.
- **`httpx`** — async + fan-out for FastAPI, aiohttp, and similar.
All three hit the same `/api/v1/notes/by-filename/<name>/append` endpoint. You'll need a Fresh Jots API token. If you don't have one, see Get & set your Fresh Jots API token. Free accounts that pick "Code" mode at onboarding get a 14-day trial token...
`pip install freshjots` — the recommended path
`pip install freshjots` — the recommended path
pip install freshjots
That's the only install line. The package is **zero runtime dependencies** — it uses `urllib` from the stdlib for HTTP, so it adds no transitive bloat to your environment.
Two-line use
Two-line use
from freshjots import Client
Client().append("deploy-log", "deploy ok — sha=abc123")`Client()` reads `FRESHJOTS_TOKEN` from the environment by default. If the variable isn't set, the constructor raises `ValueError` immediately so you find out at startup, not three hours into a long-running script.
The whole API in four methods
The whole API in four methods
from freshjots import Client
client = Client()
# 1. Append text to a note — creates it the first time.client.append("cron-jobs-prod", "backup ok")
# 2. Read a note's full plain-text body.print(client.note("cron-jobs-prod")["plain_body"])
# 3. List all your notes (summary projection).for n in client.notes(): print(f"{n['filename']}\t{n['title']}")
# 4. Create a new note explicitly (raises if the filename is taken).client.create("research-2026-q2", body="Initial outline.", title="Q2 Research")That's the entire surface — `notes()`, `note(filename)`, `create(filename, body, title)`, `append(filename, text)`. No paginators, no async, no factory classes, no plugin system. The goal of the package is to *get out of your way*.
Error handling — `ApiError`
Error handling — `ApiError`
Any non-2xx response raises `freshjots.ApiError` with a structured payload:
from freshjots import Client, ApiError
client = Client()
try:
client.append("huge-note", "x" * 5_000_000)
except ApiError as e:
print(f"{e.status} {e.code}: {e}")
# 413 content_too_large: body exceeds the per-note 3 MB capThe exception carries four useful fields:
- `e.status` — the HTTP status code (`401`, `403`, `404`, `413`, `422`, `429`, ...)
- `e.code` — a stable string code that's safe to branch on
- `str(e)` — the human-readable message
- `e.details` — optional structured payload for validation errors
Stable error codes (from the API's documented envelope): `unauthenticated`, `forbidden`, `not_found`, `validation_failed`, `cap_exceeded`, `storage_cap_exceeded`, `content_too_large`, `content_type_mismatch`, `rate_limited`. Branch on `e.code`, not the message — the code is part of the API contract; the message can be tweaked for clarity without breaking your code.
```python
try:
client.append("daily-log", entry)
except ApiError as e:
if e.code == "rate_limited":
time.sleep(60)
client.append("daily-log", entry)
elif e.code == "cap_exceeded":
# We're at the note count cap — alert ops and drop the line.
ops_alert("Fresh Jots cap exceeded; dropping log entry.")
else:
raise
Passing the token explicitly
Passing the token explicitly
If `FRESHJOTS_TOKEN` isn't right (multiple accounts, a worker that gets its token via a different secrets manager, a test that wants a known-bad token):
client = Client(token="mn_yourrealtokenhere")
Tokens are always `mn_` prefix followed by ~40 characters of random alphanumerics. Mint a real one at api tokens page.
Pointing at a different base URL (testing / staging)
Pointing at a different base URL (testing / staging)
client = Client(token="mn_test…", base_url="https://staging.freshjots.com/api/v1")
Useful for local development against a Rails server you're running yourself.
Bare HTTP with `requests` — when you don't want the package
Bare HTTP with `requests` — when you don't want the package
For a single-file script or a project that's hostile to new dependencies, `requests` gives you the same thing in five lines:
import os, requests
requests.post(
"https://freshjots.com/api/v1/notes/by-filename/deploy-log/append",
headers={"Authorization": f"Bearer {os.environ['FRESHJOTS_TOKEN']}"},
json={"text": "deploy ok — sha=abc123"},
timeout=10,
)Fine for a one-off. For anything that runs unattended, add `raise_for_status()` and a `urllib3.util.Retry` so you don't silently lose lines on transient 5xx — the [package](#pip-install-freshjots-—-the-recommended-path) does this for you, which is why it's the recommended path past the experimentation stage.
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["POST"],)))
r = session.post(
"https://freshjots.com/api/v1/notes/by-filename/deploy-log/append",
headers={"Authorization": f"Bearer {os.environ['FRESHJOTS_TOKEN']}"},
json={"text": "deploy ok"},
timeout=10,
)
r.raise_for_status()Compare that to:
Client().append("deploy-log", "deploy ok")If you're going to write the second block more than once, just install the package.
Async with `httpx` — when you're inside an async framework
Async with `httpx` — when you're inside an async framework
The `freshjots` package is intentionally sync. For async (FastAPI, aiohttp, Starlette) or for parallel fan-out, drop to `httpx` directly:
import os
import httpx
async def append_async(filename: str, text: str, *, timeout: float = 10.0) -> dict:
token = os.environ["FRESHJOTS_TOKEN"]
async with httpx.AsyncClient(timeout=timeout) as client:
r = await client.post(
f"https://freshjots.com/api/v1/notes/by-filename/{filename}/append",
headers={"Authorization": f"Bearer {token}"},
json={"text": text},
)
r.raise_for_status()
return r.json()
Fan-out — many appends, one round-trip's worth of wall time
Fan-out — many appends, one round-trip's worth of wall time
import asyncio
import httpx
import os
async def fanout(items: list[tuple[str, str]]) -> list[dict]:
"""items is a list of (filename, text). Returns one response per item."""
headers = {"Authorization": f"Bearer {os.environ['FRESHJOTS_TOKEN']}"}
async with httpx.AsyncClient(timeout=10) as client:
tasks = [
client.post(
f"https://freshjots.com/api/v1/notes/by-filename/{name}/append",
headers=headers,
json={"text": text},
)
for name, text in items
]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses if r.is_success]Don't use fan-out to spam appends at the same note — concurrent appends to one note can interleave at the byte level. For ordered writes to one note, await sequentially.
Reusing the client in long-running services
Reusing the client in long-running services
In FastAPI, instantiate one `AsyncClient` for the lifetime of the app:
```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
import httpx
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.http = httpx.AsyncClient(timeout=10)
yield
await app.state.http.aclose()
app = FastAPI(lifespan=lifespan)Now every handler can `await app.state.http.post(...)` without paying connection-pool churn per request.
Loading the token
Loading the token
Three good options, depending on how you ship:
- **Shell profile** (`~/.bashrc`, `~/.zshrc`, `~/.config/fish/config.fish`) — see [Get & set your Fresh Jots API token](/blog/getting-and-setting-the-api-token). Best for local-machine work.
- **`.env` + `python-dotenv`** — `pip install python-dotenv`, `load_dotenv()` at the top of your entry point, put `FRESHJOTS_TOKEN=mn_…` in a git-ignored `.env`. Best for project-scoped tokens and CI.
- **Container / orchestrator secret** — set it via Kubernetes, ECS, Fly.io, Docker Compose `environment:`, etc. Best for production.
The package reads `FRESHJOTS_TOKEN` once from the environment when `Client()` is instantiated. Each of the methods above gets the variable into the environment; the package doesn't care which.
Patterns that work
Patterns that work
Django request middleware
Log every request to a note. Async-frameworks-friendly variant just swaps to the `httpx` fan-out above.
```python
# myapp/middleware.py
from freshjots import Client
_fjots = Client() # one client per process
class FjotsLogger:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
try:
_fjots.append(
"django-requests",
f"{request.method} {request.path} → {response.status_code}",
)
except Exception:
# Never fail a real request because the log call failed.
pass
return responseAdd it to `MIDDLEWARE`. On a hot path, replace the inline `append` with a Celery task or a `concurrent.futures` background submit — the sync `append` adds ~50–150 ms per request, which you don't want blocking the response.
FastAPI background task
from fastapi import BackgroundTasks, FastAPI
from freshjots import Client
app = FastAPI()
fjots = Client()
@app.post("/deploy")
async def deploy(payload: dict, background: BackgroundTasks):
# ... do the deploy ...
background.add_task(fjots.append, "deploy-log", f"deploy ok — {payload['sha']}") return {"ok": True}`BackgroundTasks` runs after the response is sent, so the user doesn't wait on the network round-trip.
Celery — automatic success/failure logs for every task
```python
# celery_base.py
from celery import Task
from freshjots import Client
_fjots = Client()
class FjotsLoggedTask(Task):
"""Base class — every task that inherits writes its summary to a note."""
def on_success(self, retval, task_id, args, kwargs):
_fjots.append("celery-jobs", f"{self.name} ok ({task_id})")
def on_failure(self, exc, task_id, args, kwargs, einfo):
_fjots.append("celery-failures", f"{self.name} FAILED ({task_id}): {exc}")# tasks.py
from .celery_base import FjotsLoggedTask
from celery import shared_task
@shared_task(base=FjotsLoggedTask)
def nightly_export():
...Two notes get a running log of your job successes and failures. Pair `celery-failures` with a [dead-man's-switch alert](/blog/everything-you-can-do-here) on a longer deadline as a backstop — if `celery-failures` goes silent for 24 hours, that's either great (no failures, hooray) or bad (the worker died and isn't writing anything). Combine with `celery-heartbeat` writes from the worker startup to disambiguate.
Jupyter — "save this cell's output"
```python
import io, sys
from contextlib import redirect_stdout
from freshjots import Client
_fjots = Client()
def jot(note_name: str):
"""Cell decorator. Captures stdout and appends it to a Fresh Jots note."""
def wrap(fn):
def inner(*a, **kw):
buf = io.StringIO()
with redirect_stdout(buf):
out = fn(*a, **kw)
_fjots.append(note_name, buf.getvalue())
return out
return inner
return wrap
@jot("notebook-runs")
def explore():
print("model accuracy:", 0.91)
# ... your analysis ...Now every run of `explore()` appends its captured output to a `notebook-runs` note. Searchable from `https://freshjots.com/notes`.
pytest — log a test summary to a note
Put a session-scoped fixture in `conftest.py`:
# conftest.py
import os
import pytest
from freshjots import Client
@pytest.fixture(scope="session", autouse=True)
def fjots_test_summary():
yield # run all tests
summary = os.environ.get("PYTEST_SUMMARY", "")
if summary:
Client().append("test-runs", f"[{os.environ.get('GITHUB_SHA', 'local')}] {summary}")Combined with `pytest-html` or `pytest-json-report`, you have a searchable history of which commits passed and which didn't — without paying for a CI dashboard.
Going further
Going further
- **Source for the package:** https://github.com/Goran-Arsov/freshjots-python). MIT licensed, ~90 lines including docstrings. Easy to fork and tweak.
- **Other languages — same pattern, different HTTP client.** See the hub: Write a note from any project.
- **The CLI** — if you find yourself shelling out from Python to write notes, the Notes from your terminal does the same in one line of shell.
- **Dead-man alerts** — pair a per-script note with `append_deadline_hours` and Fresh Jots emails you when the script goes silent. See Everything you can do here.
Three lines today, four package methods tomorrow, an entire ops telemetry pipeline by next month. All over plain HTTP — no SaaS lock-in, no sidecar, no requirements.txt churn (the package itself adds zero transitive dependencies).