Metadata-Version: 2.4
Name: plcsai
Version: 1.2.0
Summary: Official Python SDK for the PLCs.ai API — interpret PLC code from your own tools.
Author: PLCs.ai
License: Proprietary
Project-URL: Homepage, https://developer.plcs.ai
Project-URL: Documentation, https://developer.plcs.ai
Keywords: plc,automation,rockwell,siemens,ai,plcs.ai
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"

# plcsai — Python SDK for the PLCs.ai API

Interpret PLC code from your own tools. Official Python client for the
[PLCs.ai API](https://developer.plcs.ai). Zero third-party dependencies.

## Install

```bash
pip install plcsai
```

Requires Python 3.8+.

## Quickstart

```python
from plcsai import Client

client = Client(api_key="plck_live_…")  # or set PLCS_API_KEY

result = client.interpret(
    project_id="prj_…",
    prompt="Why is the filler at line 2 not advancing past Starting?",
)
print(result.answer)
print(result.citations)
print(result.request_id)  # quote this in a support request
```

### Streaming

```python
for event in client.interpret_stream(project_id="prj_…", prompt="…"):
    if event.type == "token":
        print(event.text, end="", flush=True)
    elif event.type == "done":
        print("\nusage:", event.usage)
```

### Conversations, analyses, ingest, embed tokens

```python
conv = client.create_conversation("prj_…", name="Line 2 stoppage")
client.send_message(conv.conversation_id, "And why now?")

job = client.start_analysis("prj_…")
done = client.wait_for_analysis(job.analysis_id)

client.ingest_project(file_path="Conveyor.L5X")

token = client.mint_embed_token("prj_…")  # read-only, for the iframe
```

#### Billing consent (enterprise per-project orgs)

If your org is billed per project, creating a **new** project over the API
requires acknowledging the charge. Without it you get a `402` carrying the cost;
disclose it, then retry with `acknowledge_billing=True`:

```python
from plcsai import ApiError

try:
    client.ingest_project(file_path="NewLine.L5X")
except ApiError as e:
    if e.error == "billing_setup_required":
        print("Set up billing in the PLCs desktop app first.")
    elif e.error == "billing_acknowledgement_required":
        cents = e.billing["price_per_project_cents"]
        print(f"This adds a billable project (~${cents/100:.2f}). Confirming…")
        client.ingest_project(file_path="NewLine.L5X", acknowledge_billing=True)
    else:
        raise
```

This applies only to enterprise per-project orgs creating a new project — other
org types and re-uploads/new versions are unaffected.

### Projects: list, read source, live values

```python
page = client.list_projects(limit=50)
for p in page.projects:
    print(p.project_id, p.name, p.vendor)

detail = client.get_project("prj_…")          # metadata + analysis_status

src = client.get_source("prj_…", format="scl")  # parsed | scl | ladder (needs code_read)
raw_bytes = client.download_source("prj_…")      # original L5X / ZIP bytes

values = client.get_hmi_values("prj_…")          # needs hmi_view; live=False when no DCA session
history = client.get_hmi_history("prj_…", tag="Motor1.Speed")
```

### Exports (async): PLC file & PDF report

```python
job = client.export_plc("prj_…")                 # or client.export_pdf(...)
done = client.wait_for_export(job.export_id)
artifact = client.download_export(done.export_id)  # L5X / ZIP / PDF bytes
open(done.filename, "wb").write(artifact)
```

### Save a new version (code_write)

```python
res = client.commit_version("prj_…", file_path="Conveyor_edited.L5X")
print(res.resolution, res.version_id)  # add_version, or identical_file (no-op)
```

### Generate code (ai_generate — proposes, never deploys)

```python
proposal = client.generate("prj_…", "Add a 5-second start-up delay timer.")
print(proposal.generated_code)
for block in proposal.code_blocks:
    print(block.language, block.content)
# To persist, commit the edited file via client.commit_version(...) (code_write).
```

## What the client handles for you

- **Auth** — sends `Authorization: Bearer …` on every request.
- **Idempotency** — auto-generates a stable `Idempotency-Key` per write (reused across retries).
- **Retries** — backs off and retries only on retryable errors, respecting `Retry-After`.
- **Streaming** — parses SSE into typed `StreamEvent`s.
- **`request_id`** — surfaced on every result.

## Errors

Non-2xx responses raise `plcsai.ApiError` with `.status_code`, `.error`,
`.user_message`, `.suggested_action`, `.is_retryable`, and `.request_id`.

## Development

```bash
pip install -e '.[dev]'
pytest
```

## Releasing (maintainers)

`plcsai` is published to PyPI. Bump `version` in `pyproject.toml`, then:

```bash
python -m build
python -m twine upload dist/*
```
