Metadata-Version: 2.4
Name: backlot
Version: 0.0.3
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: MIT
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,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: Framework :: FastAPI
Classifier: Topic :: Software Development :: Testing :: Mocking
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
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.2
Provides-Extra: all
Requires-Dist: backlot[dev,fsspec,llamaindex,mcp,mirage,official-sdk]; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-xdist>=3.6; extra == "dev"
Requires-Dist: httpx2; extra == "dev"
Requires-Dist: ruff<0.17.0,>=0.15.22; extra == "dev"
Provides-Extra: official-sdk
Requires-Dist: slack_sdk>=3.27; 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.120; extra == "official-sdk"
Requires-Dist: google-auth>=2.29; 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.6; 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: MIT](https://img.shields.io/badge/License-MIT-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
```

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) |
| 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.
- 🐛 **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) |
| Every `BACKLOT_*` setting, and Docker | [docs/configuration.md](https://github.com/brekkylab/backlot/blob/main/docs/configuration.md) |
| Vendor names 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.

## License

[MIT](https://github.com/brekkylab/backlot/blob/main/LICENSE)
