Metadata-Version: 2.4
Name: tectly
Version: 1.0.0.post1
Summary: Python SDK for the Tectly API: turn floorplan drawings into structured building data.
Author: David Linner
License-Expression: MIT
Project-URL: Homepage, https://tectly.com
Project-URL: Source, https://gitlab.com/tectly-public/tectly-client-sdk-python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx
Requires-Dist: pydantic>=2.9
Dynamic: license-file

# Tectly Client SDK

Python SDK for the [Tectly](https://tectly.com) API. Tectly turns floorplan drawings into
structured building data: it reads scans, PDFs and images, detects the rooms, walls, doors
and windows they contain, derives the drawing scale, and returns geometry and measurements
you can process further.

- **Typed access to every public API operation**, with pydantic models generated from the
  API's own specification.
- **A progress query** that answers "have all the plans in this document been processed?"
  in one call, without polling.
- **A watch stream** that reports plans as they are recognized, so you can start working
  before the whole document is finished.
- **Synchronous and asynchronous** flavors with identical behavior.

## Install

```
pip install tectly
```

Requires Python 3.11 or newer.

## Credentials

Create an API key and secret in the [API keys dialog](https://app.tectly.com/api-keys) of
your Tectly account. The SDK defaults to the production API at
`https://platform.tectly.com/api/v1`, so credentials are all you need to supply.

## Processing a floorplan

```python
from tectly import (
    Client,
    ProjectCreate,
    UploadFile,
    DocumentPagesAvailableEvent,
    PlanDetectedEvent,
    PlanCompletedEvent,
    PlanFailedEvent,
    DocumentCompletedEvent,
)

with Client().create_session(api_key, api_secret) as session:
    project = session.create_project(ProjectCreate(title="My Project"))

    with open("floorplan.png", "rb") as handle:
        document = session.add_document(
            project.id,
            UploadFile(
                file_name="floorplan.png",
                mime_type="image/png",
                payload=handle,
            ),
        )

    print(f"Watching document {document.id} in project {project.id}")

    for event in session.watch(document.id):
        if isinstance(event, DocumentPagesAvailableEvent):
            print(f"{len(event.document.document_pages)} page(s) rendered")

        elif isinstance(event, PlanDetectedEvent):
            print(f"Found {len(event.plans)} plan(s) on page {event.document_page.id}")

        elif isinstance(event, PlanCompletedEvent):
            rooms = session.fetch_rooms(event.floor_id)
            walls = session.fetch_walls(event.floor_id)
            openings = session.fetch_wall_openings(event.floor_id)
            print(f"Floor {event.floor_id}: {len(rooms)} rooms, {len(walls)} walls")

        elif isinstance(event, PlanFailedEvent):
            print(f"Plan {event.plan_id} failed; carrying on")

        elif isinstance(event, DocumentCompletedEvent):
            print(f"Done: {event.progress.plans_done}/{event.progress.plans_total} plans")
```

A plan that fails is reported as an event, not raised: the remaining plans keep going and
the stream still ends with a `DocumentCompletedEvent`.

## Asking whether a document is finished

`watch` streams events as they happen. When you just want to know where a document has got to
— a status endpoint, a polling UI, a batch job checking back later — ask directly:

```python
from tectly import Client

with Client().create_session(api_key, api_secret) as session:
    progress = session.get_progress(document_id)

    if progress.is_complete:
        print(f"All {progress.plans_total} plan(s) processed")
    else:
        print(f"{progress.stage}: {progress.plans_done}/{progress.plans_total} done")

    for failure in progress.failed_plans:
        print(f"Plan {failure.plan_id} failed on page {failure.document_page_id}")
```

`get_progress` costs one request for the document plus one per page holding a plan. It
never sleeps or blocks.

## Resuming, and the asynchronous flavor

`watch` needs nothing but a document id, and submitting a document is a separate call that
returns one. So a process that dies mid-recognition can pick the same document up later from
the id it already has — and the same code reads naturally with `async`:

```python
from tectly import AsyncClient

async with AsyncClient().create_session(api_key, api_secret) as session:
    async for event in session.watch(document_id):
        ...
```

## Everything else

The session exposes every other public operation directly — `fetch_project`,
`create_project`, `update_room`, `add_wall`, `remove_document` and the rest — returning the
same pydantic models. Use `iter_projects()` and `iter_documents()` to walk paginated
collections without handling page numbers yourself.

Errors surface as exceptions, never as an absent return value: `AuthenticationError` (401),
`AuthorizationError` (403), `NotFoundError` (404), `ApiError` for anything else the API
returns, and `ConnectionError` when the request never reached it. All derive from
`TectlyError`.

## Development

### Regenerating the client

`src/tectly/_generated/` is a build product and must never be hand-edited. It is produced
from `spec/service.json`, which is vendored so builds are reproducible.

```
pip install -r requirements/dev.txt
curl -sSL -o spec/service.json https://platform.tectly.com/api/v1/api-docs/spec/service
python tools/generate.py
```

Operations the specification marks `x-internal` are excluded by rule rather than by a
maintained list, so endpoints the service retires drop out on the next regeneration.
`python tools/check_coverage.py` fails if the generated layer and the specification have
drifted apart; CI runs it, along with a check that regeneration is byte-for-byte stable.

### Tests

```
pip install -r requirements/test.txt
python -m pytest
```

The unit tests mock the API and need no network access or credentials. The integration
test is marked `integration` and is skipped unless `TECTLY_API_KEY` and
`TECTLY_API_SECRET` are set:

```
python -m pytest -m integration
```

### Documentation

```
pip install -r requirements/build.txt
PYTHONPATH=src pdoc -d google -o public --no-show-source tectly
```

### Releasing

Pushing a release tag builds the package, publishes it to
[PyPI](https://pypi.org/project/tectly/), and publishes the documentation. The version comes
from the tag, so `VERSION` is never edited by hand.

```
git tag v1.2.3      # or v1.2.3rc1 for a pre-release
git push origin v1.2.3
```

A tag is a release tag when it looks like `v<major>.<minor>[.<patch>]` with an optional
[PEP 440](https://peps.python.org/pep-0440/) suffix:

| Tag | Publishes as | |
|---|---|---|
| `v1.0`, `v1.2.3` | `1.0`, `1.2.3` | final release |
| `v1.2.3rc1`, `v1.2.3a1`, `v1.2.3b2` | `1.2.3rc1`, … | pre-release — `pip install` skips it unless `--pre` is given |
| `v1.2.3.dev0` | `1.2.3.dev0` | development release |
| `v1.2.3.post1` | `1.2.3.post1` | post-release |
| `v1`, `v1.2.3.4`, `v01.2.3`, `v1.2.3-rc1` | — | not a release tag; the pipeline ignores it |

The rule lives once, in `.gitlab-ci.yml`'s `.on-release-tag` template; `tools/version_from_tag.py`
derives the version from the tag and `test/test_release.py` fails if the two disagree.

Publishing needs `PYPI_API_TOKEN` as a project CI/CD variable — a PyPI API token, type
**Variable** (not File), environment scope `*`, set **masked** and **protected**.

Protecting the variable is only half of it: a protected variable is injected only into pipelines
running on a **protected ref**, and tags are not protected by default. Add the pattern `v*` under
**Settings → Repository → Protected tags**, or the publish job will find the variable empty. That
protection is also what stops anyone who can push a tag from publishing a release.

## License

MIT — see [LICENSE](LICENSE).
