Metadata-Version: 2.4
Name: rost-io
Version: 0.2.0
Summary: Adapter layer for .rost — converts any data source to canonical JSON
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: jsonschema>=4.0
Requires-Dist: polars>=1.0
Requires-Dist: fastexcel>=0.10
Requires-Dist: connectorx>=0.3
Provides-Extra: llm
Requires-Dist: instructor>=1.0; extra == "llm"
Requires-Dist: openai>=1.0; extra == "llm"
Provides-Extra: all
Requires-Dist: instructor>=1.0; extra == "all"
Requires-Dist: openai>=1.0; extra == "all"

# rost-io

Adapter layer for the **rost** roster scheduling language.  
Converts any data source into canonical rost JSON schemas, ready to feed into the `.rost` compiler.

Powered by **Polars** — fast DataFrames backed by Apache Arrow, with native Parquet I/O and Excel via `fastexcel` (calamine), SQL via `connectorx`.

---

## Installation

```bash
pip install rost-io
```

No extras needed — Polars, fastexcel, and connectorx are all core dependencies.

---

## Schemas (v0.2)

| Schema | Version | Description |
|--------|---------|-------------|
| `rost/staff/v1` | core | People, their IDs and tags |
| `rost/leave/v1` | core | Approved leave / unavailability |
| `rost/calendar/v1` | core | Scheduling horizon and public holidays |
| `rost/solution/v1` | core | Solver output — assignments per day |
| `rost/preferences/v1` | **new** | Soft shift requests from staff (wants / avoids) |
| `rost/preassignments/v1` | **new** | Manager-pinned shifts (hard constraints in MIP) |
| `rost/history/v1` | **new** | Previous period's roster (fairness continuity) |

`solution/v1` is produced by the solver; all others are inputs from adapters.

---

## Adapters

| Adapter | Source | Backend |
|---------|--------|---------|
| `CsvAdapter` | CSV files | `polars.read_csv` |
| `ExcelAdapter` | `.xlsx` / `.xls` workbooks | `polars.read_excel` + fastexcel (calamine) |
| `ParquetAdapter` | Parquet files | `polars.read_parquet` |
| `DatabaseAdapter` | PostgreSQL, MySQL, SQLite, MSSQL | `connectorx` → Polars Arrow |
| `JsonAdapter` | Pre-formatted JSON | stdlib `json` |

---

## Quick Start

### CSV

```python
from rost_io import CsvAdapter, validate_staff

adapter = CsvAdapter("staff.csv")
data = adapter.to_staff_json()
validate_staff(data)          # raises jsonschema.ValidationError on failure
print(data)
```

### Excel

```python
from rost_io import ExcelAdapter

adapter = ExcelAdapter(
    "roster_data.xlsx",
    staff_sheet="Staff",
    leave_sheet="Leave",
    preferences_sheet="Preferences",
    preassignments_sheet="Preassignments",
    history_sheet="History",
)
staff       = adapter.to_staff_json()
leave       = adapter.to_leave_json()
prefs       = adapter.to_preferences_json()
preassigns  = adapter.to_preassignments_json()
history     = adapter.to_history_json(period_start="2026-04-01", period_end="2026-04-30")
```

### Parquet

```python
from rost_io import ParquetAdapter

staff = ParquetAdapter("staff.parquet").to_staff_json()
```

### Database (PostgreSQL, MySQL, SQLite, MSSQL)

```python
from rost_io import DatabaseAdapter

adapter = DatabaseAdapter(
    "postgresql://hr:secret@db.hospital.local/staff_db",
    people_query="""
        SELECT employee_id AS id,
               full_name   AS display_name,
               department  AS tags
        FROM   employees
        WHERE  active = true
    """,
    leave_query="""
        SELECT employee_id AS person_id,
               leave_start AS start,
               leave_end   AS end,
               leave_type  AS type
        FROM   leave_requests
        WHERE  approved = true
    """,
    preferences_query="SELECT * FROM shift_preferences",
    preassignments_query="SELECT * FROM pinned_assignments",
    history_query="SELECT * FROM roster_history WHERE month = '2026-04'",
)

staff    = adapter.to_staff_json()
leave    = adapter.to_leave_json()
prefs    = adapter.to_preferences_json()
history  = adapter.to_history_json(period_start="2026-04-01", period_end="2026-04-30")
```

---

## Schema Details

### preferences/v1 — Soft shift requests

```json
{
  "schema": "rost/preferences/v1",
  "entries": [
    { "person": "alice",  "kind": "wants",  "shift": "day",  "weekday": "fri", "weight": 2.0 },
    { "person": "bob",    "kind": "avoids", "shift": "night", "note": "childcare" }
  ]
}
```

`kind` must be `"wants"` or `"avoids"`.  
`date` (YYYY-MM-DD) or `weekday` (`mon`–`sun`) or `shift` may each be present independently.

### preassignments/v1 — Manager-pinned shifts

```json
{
  "schema": "rost/preassignments/v1",
  "entries": [
    { "person": "carol", "date": "2026-05-12", "shift": "day",   "reason": "training cover" },
    { "person": "dave",  "date": "2026-05-13", "shift": "night"  }
  ]
}
```

These become hard equality constraints (`x[person][date][shift] = 1`) in the MIP.

### history/v1 — Previous period's assignments

```json
{
  "schema": "rost/history/v1",
  "period_start": "2026-04-01",
  "period_end":   "2026-04-30",
  "assignments": [
    { "person": "alice", "date": "2026-04-01", "shift": "day" }
  ]
}
```

Today's `solution/v1` output has the same `assignments[]` shape — use it directly as next month's history.

---

## Validation

```python
from rost_io import (
    validate_staff, validate_leave, validate_calendar,
    validate_preferences, validate_preassignments, validate_history,
)

validate_preferences(prefs)          # raises jsonschema.ValidationError on schema violation
validate_preassignments(preassigns)
validate_history(history)
```

---

## Custom Adapters

Subclass `RostAdapter` and implement the methods for your source:

```python
from rost_io import RostAdapter

class MyHrisAdapter(RostAdapter):
    def to_staff_json(self) -> dict:
        people = fetch_from_api()           # your logic here
        return {
            "schema": "rost/staff/v1",
            "people": [{"id": p["emp_id"], "display_name": p["name"], "tags": [], "custom": {}} for p in people],
        }
```

Methods you don't implement raise `NotImplementedError` with a helpful message.

---

## Roadmap

- **Phase 5**: Native Rust I/O crate (`rost-io-rs`) using polars-rust directly — zero-copy from Arrow to solver IR, no Python layer for batch pipelines.

---

## License

MIT — see [LICENSE](../LICENSE)
