Metadata-Version: 2.4
Name: oslp-sender-logistics
Version: 0.1.0
Summary: Publish shipping labels only the carrier's couriers can read — logistics vocabulary over the OSLP sender SDK
Project-URL: Repository, https://github.com/Open-Secure-Label-Protocol/oslp-python
License-Expression: Apache-2.0
Requires-Python: >=3.10
Requires-Dist: fpdf2>=2.7
Requires-Dist: oslp-sender<0.2,>=0.1
Requires-Dist: python-barcode>=0.15
Requires-Dist: segno>=1.6
Description-Content-Type: text/markdown

# oslp-sender-logistics

Create shipping labels from Python. The address on the label is encrypted —
only the delivery courier's approved device can read it. You don't handle keys
or crypto: you fill in a form, you get a printable PDF back.

## Install

Not on PyPI yet. Clone the two repos **side by side**, build the wheels once,
then install them into your own project like any other dependency:

```bash
git clone https://github.com/Open-Secure-Label-Protocol/oslp-pre.git
git clone https://github.com/Open-Secure-Label-Protocol/oslp-python.git
oslp-python/scripts/build-wheels.sh
```

```bash
# inside YOUR project's environment:
pip install path/to/oslp-python/dist/*.whl
```

Building needs Python 3.10+, [uv](https://docs.astral.sh/uv/), and a
[Rust toolchain](https://rustup.rs) — one component compiles from source until
published wheels exist. Your project itself needs none of that.

## Set up — two things, once

Both come from your operator's dashboard (ask whoever gave you access):

1. **`hosted-operator.properties`** — a settings file that says where to
   connect. Download it and keep it next to your code.
2. **A registration token** (`rt_…`) — lets this machine introduce itself on
   its first run. Put it in the environment:

   ```bash
   export OSLP_REGISTRATION_TOKEN=rt_...
   ```

The first run spends the token and stores this machine's identity in
`~/.oslp/machine/`. After that you never think about credentials again.

## Create a label

```python
from oslp.sender import HostedOperator
from oslp.logistics import Shipper, ShippingLabel, Address

hosted = HostedOperator.from_file("hosted-operator.properties")

with Shipper.connect(hosted) as shipper:
    label = shipper.publish(ShippingLabel(
        tracking_number="AWB-12345",
        origin=Address(name="Warehouse 7", phone="+918045006000",
                       street="Hosur Road", area="Bommasandra",
                       city="Bengaluru", state="KA", pincode="560099"),
        destination=Address(name="Rohit Sharma", phone="+919812345678",
                            street="Flat 402, Lake View Apartments", area="Powai",
                            city="Mumbai", state="MH", pincode="400076"),
        delivery_note="Call on arrival; gate code 4711.",
        carrier="dlg_...",   # your carrier's id — shipper.carriers() lists them
    ))
    label.save_artifact(".")                 # writes <label_id>.pdf — print it
    print(label.label_id, label.reprint_url)
```

That's the whole job. Keep `label_id` — it's how you refer to this label
later. `reprint_url` serves the same PDF again any time.

Build one `Shipper` when your app starts and reuse it everywhere — it's
thread-safe and long-lived.

## What's on the printed label

Readable: the tracking number and the destination's area, city, state and
pincode — enough to route the parcel. Everything else — names, phone numbers,
street addresses, your delivery note — is encrypted inside the QR code. Only a
courier device approved by the carrier can read it. Not the operator, not
anyone who photographs the box.

## Everyday tasks

| You want to | Call |
|---|---|
| See which carriers you can ship with | `shipper.carriers()` |
| Change the delivery note after printing | `shipper.update_delivery_note(label_id, "...")` — couriers see the new note on their next scan; `None` clears it |
| Publish now, pick the carrier later | connect with `dek_store=...`, publish without `carrier=`, then `shipper.add_carrier(label_id, carrier_id)` |
| Refresh the carrier list | `shipper.refresh_directory()` |

## When something goes wrong

- **`CarrierNotListedError`** — that carrier id isn't in the current list.
  Check `shipper.carriers()`, or `refresh_directory()` first.
- **`OperatorError`** — the server said no; the message says why.
- **`ValueError`** — something in your input (blank tracking number, phone not
  `+` and digits, …); the message names the field.
- **"no credentials found"** on first run — `OSLP_REGISTRATION_TOKEN` isn't
  set; see *Set up* above.

## The details, if you ever want them

Under the hood this is the OSLP protocol (the encryption is proxy
re-encryption, the label is a signed CBOR document). None of it is needed to
ship labels — but it's all in [`oslp-sender`](https://github.com/Open-Secure-Label-Protocol/oslp-python/tree/main/packages/oslp-sender) and the OSLP
hub repository when you're curious.