Metadata-Version: 2.4
Name: fulcrumpro
Version: 0.22.0
Summary: A lightweight Python SDK for the Fulcrum Pro API, built with `httpx2`.
Requires-Python: >=3.14
Description-Content-Type: text/markdown
Requires-Dist: httpx2>=2.4.0
Requires-Dist: tenacity>=9.1.4

# FulcrumPro

A lightweight Python SDK for the [Fulcrum Pro](https://fulcrumpro.com) API, built on [`httpx`](https://www.python-httpx.org/).

## Installation

```bash
pip install fulcrumpro
```

Or with `uv`:

```bash
uv add fulcrumpro
```

## Requirements

- Python 3.14+

## Quick Start

```python
from fulcrumpro import FulcrumPro

client = FulcrumPro(api_token="your-api-token")

job = client.jobs.get("6a0e5cad7c2e97225f8def9c")
print(job["name"])
```

Use as a context manager to ensure the underlying HTTP connection is closed:

```python
with FulcrumPro(api_token="your-api-token") as client:
    job = client.jobs.get("6a0e5cad7c2e97225f8def9c")
```

## Authentication

All requests are authenticated with a Bearer token. Pass your API token when constructing the client:

```python
client = FulcrumPro(api_token="your-api-token")
```

## Resources

### Jobs

```python
# Get a job by ID
job = client.jobs.get("6a0e5cad7c2e97225f8def9c")
```

### Attachments

Upload a file to a Fulcrum record. Every attachment needs an owner — the record
it hangs off — and `file` accepts a path, raw `bytes`, or an open binary file
object, with a path opened and closed for you:

```python
created = client.attachments.upload(
    "drawing.pdf",
    owner_type="job",
    owner_id="6a0e5cad7c2e97225f8def9c",
    description="Revision C drawing",
    metadata={"source": "sdk"},
)
```

Download one by ID. Omit `dest` to get the content back as `bytes`, or pass a
path to stream it straight to disk without buffering it in memory:

```python
content = client.attachments.download(created["id"])
path = client.attachments.download(created["id"], dest="drawing.pdf")
```

Find attachments on a record, and download them all as a single ZIP:

```python
owner = {"type": "job", "id": "6a0e5cad7c2e97225f8def9c"}

for attachment in client.attachments.stream(data={"owner": owner}):
    print(attachment["name"], attachment["size"])

client.attachments.download_many(data={"owner": owner}, dest="attachments.zip")
```

Queries must always be narrowed by `owner`, `owners` or `attachmentIds` — the
API will not enumerate every attachment in a tenant, and `formats` on its own
doesn't count. An unfiltered call raises `ValueError` rather than making a
request. To sweep broadly, iterate the owning records:

```python
for job in client.jobs.stream():
    owner = {"type": "job", "id": job["id"]}
    for attachment in client.attachments.stream(data={"owner": owner}):
        ...
```

`owners` takes up to 500 at a time, so batching is usually faster than one
request per record.

#### Known gap: attachments on a job operation

Files attached to a job operation — including note attachments added through the
Fulcrum UI — are stored with an owner type (`Job_ItemToMake_Operation`) that the
public API's owner enum doesn't include. It can't be sent as a filter (`list`
returns 400) and can't be read back (`get` returns 500). The failure happens
while serializing the record, so any query that would return one fails — these
attachments can't be discovered through the public API at all.

Downloads are unaffected, because they return bytes and never build an owner
object. If you obtain an ID another way, the file itself is retrievable:

```python
client.attachments.download("6aa82ae1f826405557c34006", dest="photo.png")
```

Certification attachments and remotely hosted files have their own methods:

```python
client.attachments.upload_certification(
    "cert.pdf", owner_type="item", owner_id="...", lot_id="..."
)
client.attachments.create_remote(
    owner, file_name="spec.pdf", access_url="https://example.com/spec.pdf"
)

client.attachments.delete(created["id"])
```

## Error Handling

All non-2xx responses raise a `FulcrumProError`:

```python
from fulcrumpro import FulcrumPro, FulcrumProError

client = FulcrumPro(api_token="your-api-token")

try:
    job = client.jobs.get("nonexistent-id")
except FulcrumProError as e:
    print(e.status_code)  # e.g. 404
    print(str(e))  # "FulcrumPro API error 404: not found"
```

## Version History

See [CHANGELOG.md](CHANGELOG.md).

## Docs
Full API documentation can be found on [GitHub Pages](https://cautious-adventure-v3ezn65.pages.github.io/fulcrumpro.html)
