Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- [ ] Tests added or updated
- [ ] `make check` passes locally (`ruff` + `mypy`)
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
- [ ] Documentation updated (if the public API changed)
- [ ] Documentation updated (if the public API changed), `docs/agents.md` included

## Related issues

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- run: uv sync --no-dev --group docs
- run: cp CHANGELOG.md docs/changelog.md
- run: uv run zensical build --clean
- run: uv run python scripts/emit_markdown.py
- uses: actions/upload-pages-artifact@v5
with:
path: site
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ Breaking changes: add `!` after the type (`feat!:`) or include a `BREAKING CHANG
4. Run `make check && make test-unit` locally
5. Open a PR against `master`

## The agents page

`docs/agents.md` is the whole library on one page, written for a coding assistant: the
public API, the rules that break code when they are broken, the mistakes models make, and a
map of which page to fetch for the rest. People hand it to an assistant instead of the site,
which is what makes a stale one worse than none — it teaches a model an API that no longer
exists.

It is part of the public API, so it changes in the same pull request the API does: a name
added, renamed or removed, a changed default or signature, a new rule a caller has to obey.
A new docs page means a new row in the documentation map. The review check is mechanical —
if the diff changes the public surface and `docs/agents.md` is untouched, the pull request
is not finished.

## Releasing (maintainers only)

Releases are fully automated via [Release Please](https://github.com/googleapis/release-please).
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ Production-ready idempotency library for async Python applications.

Ensure operations execute exactly once, even when called multiple times with the same idempotency key. Built for production microservices with graceful degradation, collision handling, and observability.

> [!TIP]
> **Building this with an AI assistant?** Hand it
> **[one page](https://bedrock-python.github.io/idempotency-kit/agents/)** instead of the
> whole site: the complete API surface, the rules that break code when they are broken —
> what the key actually identifies, what two concurrent callers really do, how a TTL in
> seconds is rounded — the mistakes models make with this API, and a map of which page to
> fetch for the rest. Every docs page is also served as raw Markdown at its own URL, and a
> **Copy page** button at the top of each one hands it straight to a chat window.

## Features

- **Clean Architecture** — core domain separated from infrastructure
Expand Down Expand Up @@ -119,6 +128,7 @@ Both requests get the **same result** - idempotency guaranteed!

📚 **[Full Documentation](https://bedrock-python.github.io/idempotency-kit/)**

- [For AI agents](https://bedrock-python.github.io/idempotency-kit/agents/) — the whole library on one page
- [Quick Start](https://bedrock-python.github.io/idempotency-kit/quickstart/) — get started in 5 minutes
- [User Guide](https://bedrock-python.github.io/idempotency-kit/user_guide/) — detailed usage and patterns
- [Architecture](https://bedrock-python.github.io/idempotency-kit/architecture/) — design principles
Expand Down
402 changes: 402 additions & 0 deletions docs/agents.md

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions docs/assets/javascripts/copy-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* Behaviour for the "Copy page" control.
*
* Every handler is delegated from `document`, because the theme swaps the
* content in place when instant navigation is on: a listener bound to an
* element of one page would not survive the move to the next.
*/
(function () {
"use strict";

var RESET_AFTER_MS = 2000;

/* Where the Markdown of a page is written, as an absolute URL. Two data
attributes say how far the site root is from here and where this page sits
below it; scripts/emit_markdown.py writes the file to match. The site's own
name is a third, so this file is the same in every project that carries it. */
function markdownUrl(widget) {
var base = (widget.dataset.copyBase || ".").replace(/\/$/, "");
var page = widget.dataset.copyPage || "";
var relative = page === "" ? "index.md" : page.replace(/\/$/, "") + ".md";
return new URL(base + "/" + relative, window.location.href).href;
}

function prompt(widget) {
var title = widget.dataset.copyTitle || document.title;
var site = widget.dataset.copySite || "project";
return (
"Read " +
markdownUrl(widget) +
' -- the "' +
title +
'" page of the ' +
site +
" documentation -- so I can ask questions about it."
);
}

function destination(widget, name) {
var question = encodeURIComponent(prompt(widget));
switch (name) {
case "markdown":
return markdownUrl(widget);
case "chatgpt":
return "https://chatgpt.com/?hints=search&q=" + question;
case "claude":
return "https://claude.ai/new?q=" + question;
case "perplexity":
return "https://www.perplexity.ai/search?q=" + question;
default:
return markdownUrl(widget);
}
}

function write(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
/* Insecure origins have no clipboard API; the old selection dance still
works there, which keeps a local preview usable. */
return new Promise(function (resolve, reject) {
var area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
var ok = document.execCommand("copy");
document.body.removeChild(area);
ok ? resolve() : reject(new Error("copy refused"));
});
}

function announce(widget, label, state) {
var slot = widget.querySelector("[data-copy-label]");
if (slot) slot.textContent = label;
if (state) {
widget.dataset.copyState = state;
} else {
delete widget.dataset.copyState;
}
}

function copy(widget) {
var reset = function () {
window.setTimeout(function () {
announce(widget, "Copy page", null);
}, RESET_AFTER_MS);
};
fetch(markdownUrl(widget))
.then(function (response) {
if (!response.ok) throw new Error(String(response.status));
return response.text();
})
.then(write)
.then(function () {
announce(widget, "Copied", "copied");
reset();
})
.catch(function () {
announce(widget, "Copy failed", null);
reset();
});
}

function close(widget) {
var menu = widget.querySelector("[data-copy-menu]");
var toggle = widget.querySelector("[data-copy-toggle]");
if (menu) menu.hidden = true;
if (toggle) toggle.setAttribute("aria-expanded", "false");
}

function closeAll(except) {
var widgets = document.querySelectorAll(".md-copy-page");
for (var i = 0; i < widgets.length; i++) {
if (widgets[i] !== except) close(widgets[i]);
}
}

document.addEventListener("click", function (event) {
var target = event.target;
if (!(target instanceof Element)) return;

var widget = target.closest(".md-copy-page");
if (!widget) {
closeAll(null);
return;
}
closeAll(widget);

if (target.closest("[data-copy-action]")) {
event.preventDefault();
close(widget);
copy(widget);
return;
}

var toggle = target.closest("[data-copy-toggle]");
if (toggle) {
event.preventDefault();
var menu = widget.querySelector("[data-copy-menu]");
if (!menu) return;
/* The destinations are filled in on the way out rather than at load:
the page under the widget may have changed since. */
var links = menu.querySelectorAll("[data-copy-open]");
for (var i = 0; i < links.length; i++) {
links[i].href = destination(widget, links[i].dataset.copyOpen);
}
menu.hidden = !menu.hidden;
toggle.setAttribute("aria-expanded", menu.hidden ? "false" : "true");
return;
}

if (target.closest("[data-copy-open]")) close(widget);
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape") closeAll(null);
});
})();
Loading