Metadata-Version: 2.4
Name: backlot
Version: 0.1.1
Summary: A local emulator for enterprise SaaS APIs, served over your own corpus with per-document ACLs
Author-email: nuri-yoo <yoonuri1@gmail.com>, khj809 <onsealeatang@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/brekkylab/backlot
Project-URL: Repository, https://github.com/brekkylab/backlot
Project-URL: Issues, https://github.com/brekkylab/backlot/issues
Project-URL: Changelog, https://github.com/brekkylab/backlot/releases
Project-URL: Discord, https://discord.gg/XCSsxYH6R
Keywords: mock,rag,slack,microsoft-teams,microsoft-graph,gmail,google-drive,jira,confluence,github,notion,s3,amazon-s3,hubspot,linear,graphql,benchmark
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Framework :: FastAPI
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Testing :: Mocking
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE.md
Requires-Dist: fastapi>=0.110
Requires-Dist: uvicorn[standard]>=0.29
Requires-Dist: pydantic>=2.6
Requires-Dist: pydantic-settings>=2.2
Requires-Dist: pyyaml>=6.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: jsonschema>=4.20
Requires-Dist: typer>=0.27
Requires-Dist: pyjwt[crypto]>=2.8
Requires-Dist: httpx>=0.27
Requires-Dist: graphql-core<3.3,>=3.2
Requires-Dist: xxhash>=3.0
Requires-Dist: brotli>=1.2
Requires-Dist: cryptography>=3.4
Requires-Dist: regex>=2024.4
Requires-Dist: tzdata; sys_platform == "win32"
Provides-Extra: all
Requires-Dist: backlot[dev,fsspec,llamaindex,mcp,mirage,official-sdk]; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8.4; extra == "dev"
Requires-Dist: pytest-xdist>=3.6; extra == "dev"
Requires-Dist: pytest-cov>=6; extra == "dev"
Requires-Dist: httpx2; extra == "dev"
Requires-Dist: ruff<0.18.0,>=0.15.22; extra == "dev"
Provides-Extra: official-sdk
Requires-Dist: slack_sdk>=3.43; extra == "official-sdk"
Requires-Dist: msgraph-sdk>=1.5; extra == "official-sdk"
Requires-Dist: PyGithub>=2.3; extra == "official-sdk"
Requires-Dist: atlassian-python-api>=3.41; extra == "official-sdk"
Requires-Dist: google-api-python-client>=2.198; extra == "official-sdk"
Requires-Dist: google-auth>=2.55; extra == "official-sdk"
Requires-Dist: notion-client>=3.1; extra == "official-sdk"
Requires-Dist: boto3>=1.34; extra == "official-sdk"
Requires-Dist: hubspot-api-client>=12; extra == "official-sdk"
Provides-Extra: mcp
Requires-Dist: mcp>=2; extra == "mcp"
Requires-Dist: fastmcp>=4; extra == "mcp"
Requires-Dist: httpx2; extra == "mcp"
Requires-Dist: anthropic[mcp]>=0.40; extra == "mcp"
Requires-Dist: openai>=1.40; extra == "mcp"
Requires-Dist: openai-agents>=0.1; extra == "mcp"
Provides-Extra: llamaindex
Requires-Dist: llama-index-readers-slack>=0.5; extra == "llamaindex"
Requires-Dist: llama-index-readers-google>=0.7; extra == "llamaindex"
Requires-Dist: llama-index-readers-github>=0.11; extra == "llamaindex"
Requires-Dist: llama-index-readers-jira>=0.6; extra == "llamaindex"
Requires-Dist: llama-index-readers-confluence>=0.7; extra == "llamaindex"
Requires-Dist: llama-index-readers-notion>=0.5; extra == "llamaindex"
Requires-Dist: llama-index-readers-s3>=0.6; extra == "llamaindex"
Requires-Dist: llama-index-readers-linear>=0.5; extra == "llamaindex"
Requires-Dist: jira>=3.10; extra == "llamaindex"
Requires-Dist: atlassian-python-api>=4.0; extra == "llamaindex"
Requires-Dist: slack_sdk>=3.43; extra == "llamaindex"
Requires-Dist: google-api-python-client>=2.198; extra == "llamaindex"
Requires-Dist: google-auth>=2.55; extra == "llamaindex"
Requires-Dist: boto3>=1.40; extra == "llamaindex"
Requires-Dist: s3fs>=2026.6; extra == "llamaindex"
Provides-Extra: mirage
Requires-Dist: mirage-ai[fuse,s3]>=0.0.7; python_version < "3.12" and extra == "mirage"
Requires-Dist: mirage-ai[fuse,s3]>=0.0.8; python_version >= "3.12" and extra == "mirage"
Provides-Extra: fsspec
Requires-Dist: fsspec>=2026.6; extra == "fsspec"
Requires-Dist: s3fs>=2026.6; extra == "fsspec"
Requires-Dist: gdrive-fsspec>=0.1; extra == "fsspec"
Requires-Dist: google-api-python-client>=2.198; extra == "fsspec"
Requires-Dist: google-auth>=2.55; extra == "fsspec"
Requires-Dist: pandas>=2.2; extra == "fsspec"
Dynamic: license-file

# Backlot

[![python](https://img.shields.io/pypi/pyversions/backlot)](https://pypi.org/project/backlot/)
[![PyPI](https://img.shields.io/pypi/v/backlot)](https://pypi.org/project/backlot/)
[![tests](https://github.com/brekkylab/backlot/actions/workflows/ci.yml/badge.svg)](https://github.com/brekkylab/backlot/actions/workflows/ci.yml)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/brekkylab/backlot/blob/main/LICENSE)

[![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/XCSsxYH6R)
[![X](https://img.shields.io/badge/Tweet-000000?logo=x&logoColor=white)](https://x.com/brekkylab)

**Run enterprise SaaS APIs locally.**

Backlot is a local emulator for Slack, Gmail, Google Drive, GitHub, Jira, Notion, S3 and other enterprise APIs. It reproduces the response shapes, pagination, authentication, errors and per-document access controls an integration has to handle, over a deterministic corpus you control — so you build and test against the official vendor SDKs with **no vendor account**, **no OAuth approval**, **no secrets in CI** and **no network**.

## Try it in 60 seconds

```bash
pip install backlot
backlot import --bundled   # a corpus ships with the package; nothing to fetch or write
backlot serve              # every supported API, at http://127.0.0.1:8000
```

Point an official SDK at it by changing one base URL:

```python
from slack_sdk import WebClient  # pip install slack_sdk

slack = WebClient(token="admin-service-token", base_url="http://127.0.0.1:8000/slack/api/")
print(slack.conversations_list()["channels"])
```

The same call targets Slack in production and Backlot in development. Backlot supplies the data and the credentials; your code keeps the vendor's request and response contract.

A test can run its own server instead, on a free port, with nothing to start or clean up:

```python
import backlot
from slack_sdk import WebClient

with backlot.serve() as s:  # no arguments: a tiny hello-world corpus
    slack = WebClient(token=s.token, base_url=f"{s.base_url}/slack/api/")
    channels = slack.conversations_list()["channels"]
```

### Let your coding agent run Backlot instantly

This repo is its own plugin marketplace, so the [agent skill](https://github.com/brekkylab/backlot/blob/main/skills/backlot/SKILL.md) installs with no clone and no `pip install` first:

```bash
claude plugin marketplace add brekkylab/backlot && claude plugin install backlot@brekkylab
codex plugin marketplace add brekkylab/backlot && codex plugin add backlot@brekkylab
npx skills add brekkylab/backlot --skill backlot   # any agent skills.sh installs into
```

and prompt like this:

> Mock our Slack workspace with three messages in an #incidents channel, get a server running, then show me what `conversations.history` actually returns for that channel.

## Why not use mocks?

A hand-written mock returns the response your code already expects. Backlot implements the other side of the integration, so it exposes the assumptions a mock would repeat — it is for when the behavior of the API, not just the contents of one response, is what you need to test.

| ❌ Hand-written mocks | ✅ Backlot |
|---|---|
| Test-specific response dictionaries | Vendor-shaped responses served over HTTP |
| Usually cover the happy path | Pagination, validation, auth and vendor-shaped errors |
| Custom test helpers | Official vendor SDKs and ordinary HTTP clients |
| Little or no identity model | Generated users, tokens, groups and document ACLs |
| Fixtures drift between tests | One deterministic corpus, shared locally and in CI |
| Each API mocked differently | Every API served from one process |

## What it serves

Every source on one local port, each behind the path prefix its own SDK expects, all reading one SQLite corpus.

The corpus defines the facts: messages, files, issues, authors, timestamps, threads, comments, labels, readers. Backlot derives stable ids, users, groups and tokens from them, so every run serves the same records, the same ACL-filtered views and the same pages.

It emulates the documented subset of each API it supports, not every vendor endpoint. The [endpoint-by-endpoint matrix](https://github.com/brekkylab/backlot/blob/main/docs/supported-sources.md) says which, and an implemented endpoint that diverges from the real API is a bug.

| Service | Base path | Example, on the official SDK |
|---|---|---|
| Slack | `/slack/api` | [`slack.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/slack.py) |
| Microsoft Teams | `/msgraph/v1.0` | [`msteams.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/msteams.py) |
| Gmail | `/gmail/v1` | [`gmail.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/gmail.py) |
| Google Drive (Docs, Sheets, Slides) | `/drive/v3` `/docs/v1` `/sheets/v4` `/slides/v1` | [`gdrive.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/gdrive.py) |
| GitHub | `/github` | [`github.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/github.py) |
| Jira | `/atlassian/rest/api` | [`jira.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/jira.py) |
| Confluence | `/atlassian/wiki/rest/api` | [`confluence.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/confluence.py) |
| Notion | `/notion/v1` | [`notion.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/notion.py) |
| Linear | `/linear/graphql` | [`linear/`](https://github.com/brekkylab/backlot/tree/main/examples/using-official-sdk/linear/) |
| HubSpot | `/hubspot` | [`hubspot.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/hubspot.py) |
| Fireflies | `/fireflies/graphql` | [`fireflies.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/fireflies.py) |
| Amazon S3 | `/s3` | [`s3.py`](https://github.com/brekkylab/backlot/blob/main/examples/using-official-sdk/s3.py) |

The roadmap lives in [the tracking issue](https://github.com/brekkylab/backlot/issues/89) — ask there for the source you need.

## When you need this

- 🔌 **Building or upgrading an integration**. The cursors, page shapes and error bodies the real API returns, without an account to get them from.
- 🧪 **Testing it, and keeping it tested**. One fixture on your laptop and in CI, with no secrets and nothing to flake. Every user in the corpus gets a token, so you can also assert that one caller's documents never reach another.
- 🤖 **Evaluating a RAG pipeline or an agent**. The same corpus, the same ids and the same answers on every run, so a score that moves means your code moved. Backlot supports write APIs for some vendors, so an agent that acts rather than only reads can be run here and graded on what it actually did — one endpoint reports it, another puts the corpus back for the next run ([docs/write-overlay.md](https://github.com/brekkylab/backlot/blob/main/docs/write-overlay.md)).
- 🐛 **Reproducing a bug in data you can't see**. A document inside someone else's workspace breaks your parser. Write one shaped like it, serve it, and keep the failing test.

## Bring your own corpus

The bundled corpus covers every supported service, but the main workflow is to serve your own test world: a JSONL file, one source document per line.

```jsonl
{"source_type":"slack","channel":"incidents","author_email":"bob@acme.com","created":"2026-02-10T18:00:00Z","content":"Anyone seeing 502s from the gateway?","replies":[{"content":"Looking now.","author_email":"ava@acme.com","created":"2026-02-10T18:00:40Z"}]}
```

```bash
backlot import my-corpus.jsonl --dry-run   # validate against each service's schema, touch nothing
backlot import my-corpus.jsonl && backlot serve
```

Every imported identity gets deterministic credentials, listed at `GET /_meta/users`; send the same request with another user's token to test what that caller is allowed to see. [Preparing a corpus](https://github.com/brekkylab/backlot/blob/main/docs/corpus.md) covers schemas, rosters, sharded corpora and public datasets, and [Auth and tokens](https://github.com/brekkylab/backlot/blob/main/docs/auth.md) covers each service's authentication style.

## Examples

| Point this at it | Runnable |
|---|---|
| 📦 Official vendor SDKs, one script per service | [`examples/using-official-sdk/`](https://github.com/brekkylab/backlot/tree/main/examples/using-official-sdk/) |
| 🔗 MCP servers, or Backlot's own OpenAPI→MCP and GraphQL→MCP bridges | [`examples/using-mcp-with-agents/`](https://github.com/brekkylab/backlot/tree/main/examples/using-mcp-with-agents/) |
| 🦙 Load it as documents, with the official [LlamaIndex](https://docs.llamaindex.ai/en/stable/module_guides/loading/connector/) readers | [`examples/using-llamaindex-readers/`](https://github.com/brekkylab/backlot/tree/main/examples/using-llamaindex-readers/) |
| 🐍 Read it with `pandas`, `pyarrow` or `dask`, over an [fsspec](https://filesystem-spec.readthedocs.io/) filesystem | [`examples/using-fsspec/`](https://github.com/brekkylab/backlot/tree/main/examples/using-fsspec/) |
| 🗂️ Read it with `ls`, `cat` and `grep`, over [mirage](https://github.com/strukto-ai/mirage)'s virtual filesystem | [`examples/using-mirage/`](https://github.com/brekkylab/backlot/tree/main/examples/using-mirage/) |
| 📥 Your own corpus, from a JSONL file | [`examples/bring-your-own-corpus/`](https://github.com/brekkylab/backlot/tree/main/examples/bring-your-own-corpus/) |

## Documentation

| | |
|---|---|
| Every source Backlot serves, and every endpoint of each | [docs/supported-sources.md](https://github.com/brekkylab/backlot/blob/main/docs/supported-sources.md) |
| Building a corpus, and public datasets | [docs/corpus.md](https://github.com/brekkylab/backlot/blob/main/docs/corpus.md) |
| Auth schemes and tokens | [docs/auth.md](https://github.com/brekkylab/backlot/blob/main/docs/auth.md) |
| Measuring Backlot against the real APIs | [docs/fidelity.md](https://github.com/brekkylab/backlot/blob/main/docs/fidelity.md) |
| Writes, and where they go | [docs/write-overlay.md](https://github.com/brekkylab/backlot/blob/main/docs/write-overlay.md) |
| Every `BACKLOT_*` setting, and Docker | [docs/configuration.md](https://github.com/brekkylab/backlot/blob/main/docs/configuration.md) |
| Usage telemetry: why it is collected, what is sent, and how to turn it off | [docs/telemetry.md](https://github.com/brekkylab/backlot/blob/main/docs/telemetry.md) |
| Notices and trademarks | [NOTICE.md](https://github.com/brekkylab/backlot/blob/main/NOTICE.md) |

## Contributing

See [CONTRIBUTING.md](https://github.com/brekkylab/backlot/blob/main/CONTRIBUTING.md). Fidelity to the real APIs is the point, so a divergence is a bug — measure against the real service, and bring a test that fails without your fix.

Backlot exists thanks to all the people who contribute.

<a href="https://github.com/brekkylab/backlot/graphs/contributors"><img alt="Avatars of Backlot's contributors" src="https://raw.githubusercontent.com/brekkylab/backlot/contributors/contributors.svg"></a>

## License

[Apache-2.0](https://github.com/brekkylab/backlot/blob/main/LICENSE). [NOTICE.md](https://github.com/brekkylab/backlot/blob/main/NOTICE.md) has the MIT License earlier releases were under.

<!-- mcp-name: io.github.brekkylab/backlot -->
