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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The package is not on PyPI yet.
| Security | [docs/SECURITY.md](docs/SECURITY.md) |
| Reliability | [docs/RELIABILITY.md](docs/RELIABILITY.md) |
| Full catalog | [docs/INDEX.md](docs/INDEX.md) |
| End-user agent setup | [AGENT_SETUP.md](AGENT_SETUP.md) |

## Layout

Expand Down
166 changes: 166 additions & 0 deletions AGENT_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Bild Python — agent setup playbook

You are an agent setting up the **Bild Python SDK** for a human. This file is
the only document you need. Follow it in order. Do not declare success until
the handshake at the end returns a value.

Bild is a PDM/PLM product. This library talks to `https://api.getbild.com`
using a JWT personal access token. The package is used from source (not on
PyPI yet). Repo: `https://github.com/AJFrio/Bild-Python.git`.

## Your job

1. Ask the human for the values below. Wait for answers before installing.
2. Install the library into a virtualenv.
3. Save the token to a local `.env` (never commit it, never echo it back).
4. Call **`BildClient.verify()`** — this is the required handshake.
5. Tell the human you called that function and show the exact return value.

## What to ask the human

Ask these in the human's language. Do not invent answers.

### 1. Bild API token (required)

Prompt:

> I need a Bild personal access token (JWT). An admin can create one in the
> Bild web app. Tokens shown there include issued-at, issued-by, and expiry.
> Paste the token here. I will save it to a local `.env` file and will not
> commit it or print it again.

If they do not have a token, stop. Tell them to have an admin issue one in
the Bild app, then come back. Do not guess a token. Do not ask for their
Bild password — this SDK only accepts a JWT.

### 2. Install location (required if you are not already in this repo)

Prompt:

> Do you already have [Bild-Python](https://github.com/AJFrio/Bild-Python)
> cloned on this machine? If yes, give me the folder path. If no, tell me
> which folder I should clone it into.

Default clone URL: `https://github.com/AJFrio/Bild-Python.git`

### 3. API host (optional)

Default: `https://api.getbild.com`

Only ask if they mention a custom or non-production host. If they give one,
you will pass `base_url=...` into `BildClient`. Otherwise omit it.

## Install

Use Python 3.10 or newer.

```bash
git clone https://github.com/AJFrio/Bild-Python.git
cd Bild-Python
python3 -m venv .venv
```

Activate:

- macOS / Linux: `source .venv/bin/activate`
- Windows: `.venv\Scripts\activate`

If `pip` is blocked (uv-managed interpreter):

```bash
uv pip install -e . --python .venv/Scripts/python.exe
```

Otherwise:

```bash
pip install -e .
```

If the repo is already cloned, skip `git clone` and install in that folder.

## Save the token

Copy `.env.example` to `.env` in the repo root (`.env` is gitignored):

```bash
BILD_API_KEY=<the token the human pasted>
```

Rules:

- Do not write the token into README, chat logs you control, or any tracked file.
- Do not `git add .env`.
- Prefer `.env` over putting the token in the shell, so later `BildClient()`
calls work without the human pasting it again.

If they gave a custom host, you do not need to store it unless they ask;
pass it only when constructing the client.

## Handshake (required)

From the repo root, with the venv active, run this exact code:

```python
from bild import BildClient

client = BildClient() # reads BILD_API_KEY from .env
result = client.verify()
print(result)
```

If they gave a custom host:

```python
client = BildClient(base_url="https://their-host.example")
result = client.verify()
```

`verify()` is read-only. It calls `users.list` and `projects.list` and
returns a dict shaped like:

```python
{
"ok": True,
"function": "BildClient.verify",
"base_url": "https://api.getbild.com",
"users": <API payload>,
"projects": <API payload>,
}
```

### What to tell the human after a success

Use this shape. Include the real `result` value. Do not omit it.

> Setup is complete. I called `BildClient.verify()` and it returned:
>
> ```
> <paste the exact result dict>
> ```
>
> The token works. I can use `client.api.<group>` for further Bild work.

### If it fails

| Error | Meaning | What you do |
| --- | --- | --- |
| `ValueError: Missing token` | `.env` not loaded or empty | Fix `.env`, retry `verify()` |
| `BildAuthError` (401/403) | Token invalid, expired, or wrong host | Ask for a new token; do not retry blindly |
| Other `BildAPIError` | Host or API problem | Show `status_code` and `payload`; ask the human |

Do not declare setup complete without a successful `verify()` return value.

## After setup

You may keep using the same `BildClient()` for the human's next request.

Documented groups: `users`, `projects`, `project_users`, `branches`,
`commits`, `files`, `uploads`, `checkouts`, `shared_links`, `metadata`,
`feedback`, `packages`, `revisions`, `approvals`, `boms`, `search`,
`webhooks`.

Do **not** invite users, upload, delete, checkout, or create webhooks unless
the human explicitly asked for that write. Prefer list/get/search.

Escape hatch for an unwrapped path: `client.get("projects")`.
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ BildClient
token, base_url, timeout, session
request / get / post / put / delete
resolve_branch_id / resolve_file_version
verify() — read-only handshake (users + projects)
api: _Resources
users, projects, project_users, branches, commits, files,
uploads, checkouts, shared_links, metadata, feedback,
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Python library for the [Bild External API](https://bildexternalapi.portledocs.co

> This repo is currently intended to be used from source (not published to PyPI yet).

To have an agent install this for you, give it [AGENT_SETUP.md](AGENT_SETUP.md).
It will ask for a Bild JWT, install the library, call `BildClient.verify()`,
and show you the return value.

## 1) Clone and set up

```bash
Expand Down
14 changes: 14 additions & 0 deletions bild/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ def __init__(
webhooks=WebhooksAPI(self),
)

def verify(self) -> dict[str, Any]:
"""Read-only setup handshake used by AGENT_SETUP.md.

Lists users and projects so an installing agent can prove the token
works and show the human the exact return value.
"""
return {
"ok": True,
"function": "BildClient.verify",
"base_url": self.base_url,
"users": self.api.users.list(),
"projects": self.api.projects.list(),
}

def request(self, method: str, path: str, *, params=None, json=None) -> Any:
url = f"{self.base_url}/{path.lstrip('/')}"
kwargs: dict[str, Any] = {
Expand Down
1 change: 1 addition & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| Doc | Status | What it covers |
| --- | --- | --- |
| [../ARCHITECTURE.md](../ARCHITECTURE.md) | current | Layers, HTTP contract, test map |
| [../AGENT_SETUP.md](../AGENT_SETUP.md) | current | Playbook to hand another agent for end-user install + `verify()` |
| [PRODUCT.md](PRODUCT.md) | current | Who the SDK is for and what "done" means |
| [DESIGN.md](DESIGN.md) | current | Design principles and taste |
| [CONVENTIONS.md](CONVENTIONS.md) | current | Naming, files, errors, JSON |
Expand Down
3 changes: 3 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Bild personal access tokens are JWTs. Treat them as secrets.

## Live tests

`BildClient.verify()` is read-only (`users.list` and `projects.list`). It is
the handshake in [AGENT_SETUP.md](../AGENT_SETUP.md).

`tests/test_live_api.py` and `TestLiveAuth` run only when a token is present.
They must stay read-only (list/get/search). Do not add invite, upload,
delete, checkout, or webhook-create calls to the live suite.
Expand Down
5 changes: 5 additions & 0 deletions docs/design-docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

A missing token raises `ValueError` at construct time.

`BildClient.verify()` is the consumer handshake. It is read-only
(`users.list` + `projects.list`). Installing agents follow
[AGENT_SETUP.md](../../AGENT_SETUP.md) and must show the human the exact
return value. Do not add writes to `verify()`.

## Errors

HTTP 401 and 403 raise `BildAuthError` (subclass of `BildAPIError`) with
Expand Down
3 changes: 3 additions & 0 deletions docs/product-specs/python-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Must stay aligned with `BildClient.api` and the README "API groups" list:
- Load `.env` without overriding existing env.
- Auto-resolve default branch and latest file version where documented.
- Omit `None` optional JSON fields.
- `BildClient.verify()` — read-only handshake (`users.list` + `projects.list`)
used by [AGENT_SETUP.md](../../AGENT_SETUP.md). Return shape is
`{ok, function, base_url, users, projects}`.

## Convenience (not allowed without a new spec)

Expand Down
19 changes: 19 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ def test_403_raises_auth_error(self):
with self.assertRaises(BildAuthError):
client.api.projects.list()

def test_verify_lists_users_and_projects(self):
session = RecordingSession()
client = BildClient(token="jwt-token", session=session)
result = client.verify()
self.assertEqual(
result,
{
"ok": True,
"function": "BildClient.verify",
"base_url": DEFAULT_BASE_URL,
"users": {"ok": True},
"projects": {"ok": True},
},
)
paths = [call["path"] for call in session.calls]
self.assertEqual(paths, ["/users", "/projects"])
self.assertTrue(all(call["method"] == "GET" for call in session.calls))
self.assertTrue(all(call["json"] is None for call in session.calls))


@unittest.skipUnless(os.getenv("BILD_API_KEY"), "BILD_API_KEY not set")
class TestLiveAuth(unittest.TestCase):
Expand Down
5 changes: 5 additions & 0 deletions tests/test_client_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ def last(self):
def test_full_route_coverage(self):
c = self.client

verified = c.verify()
self.assertEqual(verified["function"], "BildClient.verify")
self.assertTrue(verified["ok"])
self.assertTrue(self.last()["path"].endswith("/projects"))

c.api.users.list()
self.assertTrue(self.last()["path"].endswith("/users"))
c.api.users.invite(["a@example.com"], projects=[{"id": "p1"}])
Expand Down
7 changes: 7 additions & 0 deletions tests/test_live_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ def setUpClass(cls):
data.get("fileVersionID") or data.get("fileVersion") or data.get("versionId")
)

def test_verify_handshake(self):
result = self.client.verify()
self.assertTrue(result["ok"])
self.assertEqual(result["function"], "BildClient.verify")
self.assertTrue(result.get("users"))
self.assertTrue(result.get("projects"))

def test_list_users(self):
users = _as_list(self.client.api.users.list())
self.assertGreater(len(users), 0)
Expand Down
2 changes: 2 additions & 0 deletions tools/linters/docs_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

REQUIRED_DOCS = (
"AGENTS.md",
"AGENT_SETUP.md",
"ARCHITECTURE.md",
"docs/INDEX.md",
"docs/PRODUCT.md",
Expand Down Expand Up @@ -33,6 +34,7 @@
"ARCHITECTURE.md",
"docs/CONVENTIONS.md",
"docs/INDEX.md",
"AGENT_SETUP.md",
"tools/check.py",
"Content-Type",
)
Expand Down
Loading