Metadata-Version: 2.4
Name: datalastic
Version: 0.2.0
Summary: Python SDK for the Datalastic Maritime API
Author: Datalastic
License: MIT License
        
        Copyright (c) 2026 Datalastic
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://datalastic.com
Project-URL: Documentation, https://datalastic.com/documentation-api/
Project-URL: Repository, https://github.com/datalastic/datalastic-python
Keywords: datalastic,maritime,vessel,ais,ports,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# Datalastic Python SDK

The official Python library for the [Datalastic](https://datalastic.com) Maritime API. This Python maritime API client covers real-time AIS vessel tracking, port search, sea route calculation, maritime intelligence data, and asynchronous report management.

## Installation

```bash
pip install datalastic
```

Requires Python 3.8 or later and the `requests` library.

## Quick start

```python
from datalastic import Client

client = Client("YOUR_API_KEY")

# Real-time position of a single vessel
vessel = client.vessels.get(mmsi="636092297")
print(vessel.name, vessel.lat, vessel.lon, vessel.speed)

# Account usage
stat = client.stat()
print(stat.requests_remaining)
```

## Authentication

Every request includes your API key as the `x-api-key` HTTP header, set once on the session when you construct the client. You never need to set headers yourself. Pass the key once when constructing the client:

```python
client = Client("YOUR_API_KEY")

# Optional: override the default 30-second timeout
client = Client("YOUR_API_KEY", timeout=10)
```

## Timeouts and retries

Transient failures are retried for you. By default the client retries connection errors, request timeouts, and HTTP 429 up to three times with exponential backoff.

```python
client = Client(
    "YOUR_API_KEY",
    timeout=30,             # per-request timeout, seconds
    max_retries=3,          # 0 disables retrying entirely
    backoff_factor=0.5,     # waits 0.5s, 1s, 2s between attempts
    retry_after_max=60,     # ceiling for any single wait, seconds
    retry_on_status=(429,), # statuses worth repeating
)
```

| Setting | Default | Notes |
|---|---|---|
| `timeout` | `30` | Per-request timeout in seconds. Must be a positive number; `None` is rejected so a request can never hang forever. |
| `max_retries` | `3` | Maximum retries per request. `0` sends exactly one request, as in v0.1.0. |
| `backoff_factor` | `0.5` | Wait before retry *n* (zero-based) is `backoff_factor * 2 ** n`. |
| `retry_after_max` | `60` | Ceiling in seconds for every wait, including one derived from a `Retry-After` header. Nothing waits longer than this. |
| `retry_on_status` | `(429,)` | Only 408, 429, and 5xx may be listed. Passing 401, 402, 404, or any other client error raises `ValueError`. |

Details worth knowing:

- **`Retry-After` is honored on 429.** Both RFC 7231 forms are supported: delta-seconds (`120`) and an HTTP-date (`Wed, 21 Oct 2026 07:28:00 GMT`). A date in the past means "retry now". A missing, malformed, or absurd header falls back to the backoff schedule, and every wait is capped at `retry_after_max`.
- **Client errors are never retried.** 401, 402, 404, and other 4xx statuses describe a problem with the request; repeating it cannot help. They raise immediately.
- **5xx is not retried by default.** The SDK cannot tell a transient upstream blip from a deterministic server-side failure, and retrying every 5xx amplifies load during an incident. If you sit behind a gateway that returns occasional 502/503/504, opt in with `retry_on_status=(429, 502, 503, 504)`.
- **POST is treated as non-idempotent.** `reports.submit` creates a report, so a timeout or dropped connection is *not* retried: the request may well have reached the server, and repeating it would risk a duplicate report. A retryable status *is* retried on POST, because the server answered and therefore refused the request rather than performing it.
- When retries are exhausted the original error surfaces: `RateLimitError` if the last attempt was a 429, otherwise an `APIError` carrying the endpoint and the number of attempts made.

## Response metadata and pagination

Every method returns the parsed `data` payload, and every returned object also carries the response envelope on `.meta` — pagination tokens, credit and usage counters, and the API's `success` flag.

```python
vessel = client.vessels.get(mmsi="636092297")
print(vessel.name)     # unchanged
print(vessel.meta)     # {'success': True, ...}
```

Methods that return a collection return a `ResultList`. That is a real `list` subclass: iteration, indexing, `len()`, slicing, and equality with a plain `list` all work exactly as before. It just adds `.meta` and `.next_token`.

```python
ports = client.ports.find(country_iso="NL")
print(len(ports), ports[0].port_name)   # ordinary list access
print(ports.meta)                       # the envelope
```

`.meta` is always a dict — an empty one when a response carries no envelope, so `result.meta.get("...")` is always safe.

### Pagination

`vessels.find` and `vessels.in_radius` are paginated. Read `.next_token` off the result and pass it back as `next` to fetch the following page. `.next_token` is `None` on the last page.

```python
page = client.vessels.find(name="MAERSK", vessel_type="cargo")
vessels = list(page)

while page.next_token:
    page = client.vessels.find(
        name="MAERSK", vessel_type="cargo", next=page.next_token
    )
    vessels.extend(page)

print(len(vessels))
```

`vessels.in_radius` returns a single `VesselInRadiusResult` rather than a list, but exposes the same `.next_token`:

```python
page = client.vessels.in_radius(port_unlocode="NLRTM", radius=20)
found = list(page.vessels)

while page.next_token:
    page = client.vessels.in_radius(
        port_unlocode="NLRTM", radius=20, next=page.next_token
    )
    found.extend(page.vessels)
```

## Vessels

The Datalastic Python SDK exposes eight vessel methods. At least one of `uuid`, `mmsi`, or `imo` is required for all single-vessel lookups.

### `vessels.get` -- real-time basic tracking

```python
vessel = client.vessels.get(mmsi="636092297")
vessel = client.vessels.get(imo="9320403")
vessel = client.vessels.get(uuid="...")
```

Returns a `Vessel`.

### `vessels.pro` -- real-time pro tracking

Adds draught, departure/destination ports, ATD, and ETA fields.

```python
vessel = client.vessels.pro(mmsi="636092297")
print(vessel.current_draught, vessel.dest_port, vessel.eta_UTC)
```

Returns a `VesselPro`.

### `vessels.bulk` -- lookup many vessels at once

Pass a list or a comma-separated string for each identifier type. Identifiers are sent as repeated query parameters.

```python
result = client.vessels.bulk(mmsi=["636092297", "538005989"])
result = client.vessels.bulk(imo="9320403,9301491")
print(result.total)
for vessel in result.vessels:
    print(vessel.name)
```

Returns a `VesselBulkResult`.

### `vessels.in_radius` -- find vessels near a point, port, or vessel

`radius` (nautical miles) is required. An anchor is also required: either `lat` + `lon`, `port_uuid`, `port_unlocode`, `uuid`, `mmsi`, or `imo`. Optional filters include `type`, `type_specific`, `exclude`, `nav_status`, `next`, and `empty`.

```python
# By coordinates
near = client.vessels.in_radius(lat=51.95, lon=4.14, radius=50, type="cargo")

# By port UN/LOCODE
near = client.vessels.in_radius(port_unlocode="NLRTM", radius=20)

# Include vessels with no recent AIS position
near = client.vessels.in_radius(lat=51.95, lon=4.14, radius=50, empty=True)
```

Returns a `VesselInRadiusResult`.

### `vessels.history` -- historical track

```python
history = client.vessels.history(mmsi="636092297", days=7)
history = client.vessels.history(
    imo="9320403", from_date="2026-01-01", to_date="2026-02-01"
)
for point in history.positions:
    print(point["lat"], point["lon"])
```

`from_date` and `to_date` are sent to the API as `from` and `to`. Returns a `VesselHistory`.

### `vessels.info` -- vessel specifications

```python
info = client.vessels.info(imo="9320403")
print(info.gross_tonnage, info.deadweight, info.year_built)
```

Returns a `VesselInfo`.

### `vessels.find` -- search the vessel database

At least one real search criterion is required. `fuzzy` and `next` alone do not qualify. `vessel_type` is sent to the API as the `type` parameter. Supports the `empty` boolean to include vessels with no recent AIS data.

```python
matches = client.vessels.find(name="MAERSK", vessel_type="cargo")
matches = client.vessels.find(
    country_iso="DK",
    gross_tonnage_min=50000,
    year_built_min=2010,
)
# Include vessels not currently transmitting
matches = client.vessels.find(name="NORDIC", empty=True)
```

Returns a `ResultList` of `VesselInfo` (a `list` that also carries `.meta` and `.next_token`).

### `vessels.estimated` -- SAT-E estimated position (add-on)

Uses the extended base URL. Returns the latest satellite-estimated position when terrestrial AIS is unavailable.

```python
est = client.vessels.estimated(mmsi="636092297")
print(est.estimated_position)
```

Returns a `VesselEstimated`.

## Ports

### `ports.find` -- search ports

At least one parameter is required. `lat` and `lon` must be provided together.

```python
ports = client.ports.find(name="ROTTERDAM")
ports = client.ports.find(country_iso="NL")
ports = client.ports.find(lat=51.95, lon=4.14, radius=30)
ports = client.ports.find(unlocode="NLRTM")
ports = client.ports.find(port_type="sea")
```

Returns a `ResultList` of `Port`.

### `ports.get` -- retrieve a single port with terminal detail

```python
port = client.ports.get(unlocode="NLRTM")
print(port.port_name, len(port.terminals))

port = client.ports.get(name="ROTTERDAM")
```

Returns a `PortDetail`.

## Sea routes (add-on)

### `routes.calculate`

Calculates the sea route between an origin and a destination. Each endpoint can be specified as a coordinate pair, a port UUID, or a port UN/LOCODE.

```python
# By coordinates
route = client.routes.calculate(
    lat_from=51.95, lon_from=4.14,
    lat_to=40.65, lon_to=-74.05,
)

# By UN/LOCODE
route = client.routes.calculate(
    port_unlocode_from="NLRTM",
    port_unlocode_to="USNYC",
)

print(route.from_point)
print(route.to_point)
print(route.route["total_dist"])
```

Returns a `SeaRoute`.

## Maritime intelligence (add-ons)

All intel methods call the `maritime_reports` base URL and return a `ResultList` of typed records.

### `intel.dry_dock`

Dry dock schedule records. Filter by `imo`, `name`, `dry_dock_from`, or `dry_dock_to`.

```python
records = client.intel.dry_dock(imo="9320403")
for r in records:
    print(r.vessel_name, r.dry_dock_date, r.country_name)
```

Returns a `ResultList` of `DryDockRecord`.

### `intel.casualties`

Vessel casualty records. Filter by `imo`, `name`, `from_date`, or `to_date`.

```python
records = client.intel.casualties(
    imo="9320403", from_date="2025-01-01", to_date="2026-01-01"
)
```

Returns a `ResultList` of `CasualtyRecord`.

### `intel.inspections`

Port state control inspection records. Filter by `imo`, `name`, `from_date`, or `to_date`.

```python
records = client.intel.inspections(imo="9320403")
for r in records:
    print(r.inspection_date, r.inspection_authority, r.detention)
```

Returns a `ResultList` of `InspectionRecord`.

### `intel.spd`

Sale, purchase, and demolition records. Filter by `imo`, `name`, `from_date`, or `to_date`.

```python
records = client.intel.spd(imo="9320403")
for r in records:
    print(r.sales_report_date, r.buyer, r.sales_price_usd_mio)
```

Returns a `ResultList` of `SPDRecord`.

### `intel.ownership`

Beneficial ownership and management records. Filter by `imo`, `name`, `beneficial_owner`, `operator`, `technical_manager`, `commercial_manager`, or `updated_from`.

```python
records = client.intel.ownership(beneficial_owner="MAERSK")
for r in records:
    print(r.vessel_name, r.beneficial_owner_country, r.technical_manager)
```

Returns a `ResultList` of `OwnershipRecord`.

### `intel.class_society`

Classification society records. Filter by `imo`, `name`, `fuzzy`, `beneficial_owner`, `beneficial_owner_imo`, `technical_manager`, `technical_manager_imo`, or `updated_from`.

```python
records = client.intel.class_society(imo="9320403")
for r in records:
    print(r.class1_code, r.special_survey_date)
```

Returns a `ResultList` of `ClassSocietyRecord`.

### `intel.engine`

Engine and propulsion records. Filter by `imo`, `name`, `fuzzy`, or `updated_from`.

```python
records = client.intel.engine(imo="9320403")
for r in records:
    print(r.engine_designation, r.engine_builder, r.mco, r.mco_unit)
```

Returns a `ResultList` of `EngineRecord`.

### `intel.companies`

Maritime company records. Filter by `company_imo`, `name`, or `updated_from`.

```python
records = client.intel.companies(company_imo="0123456")
for r in records:
    print(r.short_name, r.company_type, r.country_code)
```

Returns a `ResultList` of `CompanyRecord`.

## Reports

Reports are asynchronous. Submit a request, then poll by ID until the status is `"done"`.

### `reports.submit`

Pass extra report parameters as keyword arguments.

```python
report = client.reports.submit(
    "inradius_history",
    port_uuid="...",
    days=30,
)
print(report.report_id, report.status)
```

Valid report types:

| Type | Description |
|---|---|
| `inradius_history` | Historical positions within a radius |
| `vessel_list` | Filtered vessel list |
| `port_list` | Filtered port list |
| `request_usage` | Account request usage |
| `dry_dock_dates` | Dry dock schedule export |
| `casualty` | Casualty data export |
| `inspections` | PSC inspection data export |
| `sales_purchase_demolitions` | SPD transactions export |
| `ownership` | Ownership data export |
| `class_society` | Classification society export |
| `engine` | Engine data export |
| `companies` | Company data export |

Returns a `Report`.

### `reports.get`

Poll the status of a single report.

```python
report = client.reports.get(report.report_id)
if report.status == "done":
    print(report.result_url)
```

Returns a `Report`.

### `reports.list_all`

List every report associated with the API key.

```python
for r in client.reports.list_all():
    print(r.report_id, r.report_type, r.status)
```

Returns a `ResultList` of `Report`.

## Error handling

All exceptions inherit from `DatalasticError`.

```python
from datalastic import (
    Client,
    DatalasticError,
    AuthenticationError,
    InsufficientCreditsError,
    NotFoundError,
    RateLimitError,
    APIError,
)

client = Client("YOUR_API_KEY")

try:
    vessel = client.vessels.get(mmsi="636092297")
except AuthenticationError:
    print("Invalid or expired API key")
except InsufficientCreditsError:
    print("Account credits exhausted")
except NotFoundError:
    print("Vessel not found")
except RateLimitError:
    print("Rate limit exceeded")
except APIError as exc:
    print("API error", exc.status_code, exc)
except DatalasticError:
    print("Other SDK error")
```

| Exception | HTTP status | When raised |
|---|---|---|
| `AuthenticationError` | 401 | Invalid or expired API key |
| `InsufficientCreditsError` | 402 | No credits remaining |
| `NotFoundError` | 404 | Resource does not exist |
| `RateLimitError` | 429 | More than 600 requests per minute |
| `APIError` | other / timeout | Any other error; carries `status_code` |
| `DatalasticError` | -- | Base class for all of the above |

Input validation errors (`ValueError`) are raised before any HTTP request is made, so they are never wrapped in a `DatalasticError`. Invalid client configuration -- a negative `max_retries`, a non-numeric `backoff_factor`, a non-retryable entry in `retry_on_status` -- is also rejected with `ValueError` at construction, before the client can be used.

### Logical failures on HTTP 200

The API can answer with HTTP 200 and still report a failure in the envelope. When `meta.success` is explicitly false the SDK raises `APIError` rather than handing back an empty model, and includes `meta.message` and the endpoint in the error text:

```python
# {"meta": {"success": false, "message": "Vessel not in coverage"}}
try:
    vessel = client.vessels.get(mmsi="636092297")
except APIError as exc:
    print(exc)   # [200] API reported failure (GET .../vessel): Vessel not in coverage
```

Responses with no `meta` at all, or a `meta` without a `success` key, are treated as successful -- endpoints vary, and only an explicit falsy `success` is an error.

Every model exposes the raw API payload as `.raw` for fields not surfaced as named attributes, and the response envelope as `.meta`.

## Model reference

Every model listed below carries two fields in addition to the named ones: `raw`, the untouched API payload, and `meta`, the response envelope (plus the derived `next_token` property). `ResultList` -- returned by every collection endpoint -- is a `list` subclass carrying the same `meta` and `next_token`.

### Vessel

| Field | Type | Notes |
|---|---|---|
| `uuid` | str | Datalastic vessel UUID |
| `name` | str | Vessel name |
| `mmsi` | str | MMSI identifier |
| `imo` | str | IMO number |
| `eni` | str | ENI number (inland waterways) |
| `country_iso` | str | Flag state ISO code |
| `type` | str | Vessel type |
| `type_specific` | str | Specific vessel type |
| `lat` | float | Last known latitude |
| `lon` | float | Last known longitude |
| `speed` | float | Speed over ground (knots) |
| `course` | float | Course over ground (degrees) |
| `navigation_status` | str | AIS navigational status |
| `heading` | float | True heading (degrees) |
| `destination` | str | AIS-reported destination |
| `last_position_epoch` | int | Unix timestamp of last AIS fix |
| `last_position_UTC` | str | UTC datetime of last AIS fix |
| `eta_epoch` | int | ETA as Unix timestamp |
| `eta_UTC` | str | ETA as UTC datetime string |
| `raw` | dict | Full API response payload |

### VesselPro

All `Vessel` fields plus:

| Field | Type | Notes |
|---|---|---|
| `current_draught` | float | Current draught (metres) |
| `dest_port` | str | Destination port name |
| `dest_port_unlocode` | str | Destination port UN/LOCODE |
| `dep_port` | str | Last departure port name |
| `dep_port_unlocode` | str | Last departure port UN/LOCODE |
| `atd_epoch` | int | Actual time of departure as Unix timestamp |
| `atd_UTC` | str | Actual time of departure as UTC string |

### VesselEstimated

All `VesselPro` fields plus:

| Field | Notes |
|---|---|
| `estimated_position` | SAT-E estimated position data |

### VesselBulkResult

| Field | Notes |
|---|---|
| `total` | Count of matching vessels |
| `vessels` | `list[Vessel]` |
| `raw` | Full API response payload |

### VesselInRadiusResult

| Field | Notes |
|---|---|
| `point` | The anchor point used for the search |
| `total` | Count of vessels found |
| `vessels` | `list[Vessel]` (each vessel's `.raw` includes `distance`) |
| `raw` | Full API response payload |

### VesselHistory

`uuid`, `name`, `mmsi`, `imo`, `eni`, `country_iso`, `type`, `type_specific`, `positions` (list of position dicts), `raw`

### VesselInfo

`uuid`, `name`, `name_ais`, `mmsi`, `imo`, `eni`, `country_iso`, `country_name`, `callsign`, `type`, `type_specific`, `gross_tonnage`, `deadweight`, `teu`, `liquid_gas`, `length`, `breadth`, `draught_avg`, `draught_max`, `speed_avg`, `speed_max`, `year_built`, `is_navaid`, `home_port`, `raw`

### Port

`uuid`, `port_name`, `country_iso`, `country_name`, `unlocode`, `port_type`, `lat`, `lon`, `area_lvl1`, `area_lvl2`, `raw`

### PortDetail

All `Port` fields plus `terminals` (list).

### SeaRoute

| Field | Notes |
|---|---|
| `from_point` | Origin point data from the API |
| `route` | Route details dict (includes `total_dist`) |
| `to_point` | Destination point data from the API |
| `raw` | Full API response payload |

### ApiStat

`user_id`, `key_status`, `requests_made`, `requests_remaining`, `raw`

### Report

`report_id`, `report_type`, `status`, `result_url`, `created_at`, `updated_at`, `params`, `raw`

### DryDockRecord

`id`, `imo`, `vessel_name`, `special_survey_date`, `dry_dock_date`, `iopp_issue_date`, `iopp_exp_date`, `technical_manager`, `country_code`, `country_name`, `website`, `email`, `phone`, `address`, `linkedin`, `modified_at`, `raw`

### CasualtyRecord

`id`, `imo`, `vessel_name`, `casualty_date`, `casualty_type`, `casualty_details`, `modified_at`, `raw`

### InspectionRecord

`id`, `imo`, `vessel_name`, `vessel_type_code`, `flag_code`, `inspection_date`, `inspection_authority`, `inspection_port`, `inspection_type`, `detention`, `ship_deficiencies`, `deficiency_description`, `technical_ism_manager`, `country_code`, `website`, `email`, `phone`, `address`, `company_imo`, `modified_at`, `raw`

### SPDRecord

`id`, `sales_report_date`, `imo`, `vessel_name`, `flag_name`, `vessel_type_code`, `built_year`, `dwt_design`, `gt`, `ldt`, `seller`, `buyer`, `sales_price_usd_mio`, `sales_price_usd_per_ldt`, `destination`, `sales_type`, `dry_dock_date`, `special_survey_date`, `sales_note`, `previous_sales_record`, `modified_at`, `raw`

### OwnershipRecord

`id`, `imo`, `vessel_name`, `beneficial_owner`, `beneficial_owner_country`, `operator`, `operator_country`, `flag_name`, `vessel_type_code`, `built_year`, `buyer`, `dwt_design`, `class1_code`, `technical_manager`, `technical_manager_country`, `commercial_manager`, `commercial_manager_country`, `modified_at`, `raw`

### ClassSocietyRecord

`imo`, `vessel_name`, `vessel_type_code`, `flag_name`, `built_year`, `dwt_design`, `special_survey_date`, `dry_dock_date`, `class1_code`, `beneficial_owner_imo`, `beneficial_owner`, `technical_manager_imo`, `technical_manager`, `draft_design`, `nt`, `gt`, `loa`, `lbp`, `depth`, `beam_moduled`, `engine_builder`, `engine_designer`, `propulsion_type_code`, `modified_at`, `raw`

### EngineRecord

`imo`, `vessel_name`, `vessel_type_code`, `propulsion_type_code`, `mco`, `mco_unit`, `mco_rpm`, `trading_category_code`, `built_year`, `gt`, `engine_designation`, `engine_builder`, `engine_designer`, `modified_at`, `raw`

### CompanyRecord

`id`, `short_name`, `long_name`, `company_type`, `country_code`, `company_imo`, `website`, `company_status`, `email`, `phone`, `address`, `linkedin`, `parent_company_imo`, `parent_company_name`, `modified_at`, `raw`

## License

MIT. See [LICENSE](LICENSE).
