Metadata-Version: 2.4
Name: statica-reporter
Version: 0.1.0
Summary: TUI for batch IDEA StatiCa connection reporting and white-label export
Requires-Python: >=3.13
Requires-Dist: ideastatica-connection-api
Requires-Dist: kaleido>=1.3.0
Requires-Dist: lxml>=5.0.0
Requires-Dist: plotly>=6.8.0
Requires-Dist: python-docx>=1.1.2
Requires-Dist: textual>=1.0.0
Description-Content-Type: text/markdown

# statica-reporter

A terminal UI (TUI) for batch IDEA StatiCa connection reporting. Browse multiple
`.ideaCon` projects and their connections in a tree, glance at per-connection
details, build an ordered report selection, and export in four formats.

## Run

```bash
# TUI (interactive)
uvx --from . statica-reporter          # before publishing
# uvx statica-reporter                 # once published to PyPI

# Headless batch (preserves the original main.py workflow)
uvx --from . statica-reporter-batch --input-dir inputs --output-dir out --json
```

Requires a running IDEA StatiCa Connection REST service. The TUI attaches to one
at `http://localhost:5000` by default; if none is reachable it offers to start a
detected local installation (or accept a custom URL).

## Features

- **Browse**: tree of projects → connections with status glyphs; lazy two-pane
  detail (status + per-category checks, design forces, geometry counts). Press
  `Enter` to fetch, `c` to run CBFEM analysis.
- **Report builder**: move connections from an *Available* tree into an ordered
  *Selected* list (`Space`/`Enter` add, `A` add all, `J`/`K` reorder, `d` remove).
- **Export** (combined by default, optional split-per-connection):
  1. StatiCa **PDF** (native multi-connection report, grouped per project)
  2. Connection **summary docx** (one table: ID, isometric view, forces, checks)
  3. **JSON** data dump (raw results)
  4. **Bound white-label docx** — all selected reports concatenated and run through
     Pandoc with a blank reference document, so headings/tables map to stock Word
     styles and inherit a company template on paste (with TOC + bookmarks).
- **Workspaces**: save/open named workspaces (added projects, selection, output
  dir, export prefs) under `%APPDATA%/statica-reporter/workspaces`.

## Architecture

- `statica_reporter/core/` — UI-free engine (service, client, analysis, exports,
  rendering, summary_docx, bound_report). Fully unit-tested without a live service.
- `statica_reporter/tui/` — Textual app, screens, widgets.
- `statica_reporter/cli.py` — headless batch entry point.
- `statica_reporter/workspace.py` — named workspace persistence.

Run the tests with `uv run pytest`.

> The legacy root-level `main.py` and `report_generator.py` are superseded by the
> package (migrated into `core/`); they remain only for reference and can be removed.

---

## IDEA StatiCa Connection API — Developer Reference

This document captures everything learned from working against the live API (server v26.0.1.2450, Python client v26.0.0.3314) so future development doesn't have to rediscover it.

---

## Setup

### Install the client

```bash
uv add ideastatica-connection-api
# or: pip install ideastatica-connection-api
```

### Prerequisites

The API requires a running IDEA StatiCa REST service. In this project the server is already running at `http://localhost:5000`. Docs for the running instance are available at `http://localhost:5000/api`.

---

## Connecting

Use `ConnectionApiServiceAttacher` as a context manager. It handles session lifecycle automatically (client registration and cleanup on exit).

```python
import ideastatica_connection_api.connection_api_service_attacher as connection_api_service_attacher

BASE_URL = "http://localhost:5000"

with connection_api_service_attacher.ConnectionApiServiceAttacher(BASE_URL).create_api_client() as api_client:
    # all work happens here
    pass
```

The `api_client` object exposes every API group as an attribute:

| Attribute | Purpose |
| --- | --- |
| `api_client.project` | Open, close, query projects |
| `api_client.connection` | Query and modify connections |
| `api_client.calculation` | Run analysis, fetch results |
| `api_client.report` | Export PDF / Word / HTML |
| `api_client.load_effect` | Manage load cases |
| `api_client.material` | Steel, bolt, weld, concrete materials |
| `api_client.member` | Connection members |
| `api_client.operation` | Connection operations (welds, bolts, etc.) |
| `api_client.parameter` | Parametric values |
| `api_client.template` | Connection templates |
| `api_client.settings` | Project settings |
| `api_client.export` | IOM / IFC export |
| `api_client.presentation` | 3D scene data |

---

## Core workflow

### 1. Open a project

```python
api_client.project.open_project_from_filepath(r"C:\path\to\file.ideaCon")
project_id = api_client.project.active_project_id  # str UUID, set automatically
```

### 2. List connections

```python
project_data = api_client.project.get_project_data(project_id)
# project_data.connections -> list of ConConnection
for conn in project_data.connections:
    print(conn.id, conn.name)   # id is an int
```

### 3. Check if a connection is already analyzed

There is no explicit "status" field. Check by calling `get_results` and inspecting `check_res_summary`:

```python
results = api_client.calculation.get_results(project_id, [conn_id])
already_analyzed = (
    results
    and results[0]
    and results[0].check_res_summary is not None
)
```

`ConnectionCheckRes` fields: `check_res_summary`, `check_res_plate`, `check_res_weld`, `check_res_bolt`, `check_res_anchor`, `check_res_concrete_block`, `buckling_results`, `name`, `connection_id`, `id`, `messages`.

### 4. Run analysis

```python
summaries = api_client.calculation.calculate(project_id, [conn_id])
# returns List[ConResultSummary]
# ConResultSummary fields: id, passed, result_summary
```

`calculate` accepts a plain `List[int]` of connection IDs — pass multiple IDs to batch.

> **Note:** The official documentation shows a `ConCalculationParameter` object, but this class does not exist in the installed Python client. Pass a plain list directly.

### 5. Fetch raw JSON results

```python
raw_list = api_client.calculation.get_raw_json_results(project_id, [conn_id])
# returns List[str], one JSON string per connection ID
import json
data = json.loads(raw_list[0])
```

Top-level keys in the returned object: `codeType`, `plates`, `platesDeformation`, `bolts`, `welds`, `anchors`, `concreteBlock`, `bucklingResults`, and others depending on connection type.

### 6. Fetch structured results

```python
results = api_client.calculation.get_results(project_id, [conn_id])
# returns List[ConnectionCheckRes]
summary = results[0].check_res_summary   # ConResultSummary
plates  = results[0].check_res_plate     # list of CheckResPlate
welds   = results[0].check_res_weld      # list of CheckResWeld
bolts   = results[0].check_res_bolt      # list of CheckResBolt
```

### 7. Export reports

```python
# PDF — returns bytes, write manually
pdf_bytes = api_client.report.generate_pdf(project_id, conn_id)  # conn_id is int
with open("report.pdf", "wb") as f:
    f.write(pdf_bytes)

# Word
docx_bytes = api_client.report.generate_word(project_id, conn_id)

# HTML zip
zip_bytes = api_client.report.generate_html_zip(project_id, conn_id)

# Batch PDF (all connections at once)
# Note: "mutliple" is a typo in the API itself — use it exactly as shown
pdf_bytes = api_client.report.generate_pdf_for_mutliple(project_id, [conn_id_1, conn_id_2])
```

> **Note:** The return type annotation says `None` for `generate_pdf` — this is wrong. It returns `bytes`.

### 8. Close the project

```python
api_client.project.close_project(project_id)
```

Always close when done. Unclosed projects remain in the server's memory.

---

## Units

The API **never returns unit labels**. There are no unit-related classes, settings, or fields anywhere in the package. All raw numeric values are in **SI units**, regardless of the project's design code (American, Eurocode, etc.):

| Quantity | Unit |
| --- | --- |
| Length / dimension | m |
| Stress / pressure / strength | Pa |
| Force | N |
| Moment | N·m |

Example cross-check from a real result (`codeType: "american"`, material A992):

- `thickness: 0.04` → 40 mm plate ✓
- `materialFy: 344_740_000` → 344.7 MPa ≈ 50 ksi ✓
- `materialModulusOfElasticity: 200_000_000_000` → 200 GPa ✓

---

## Settings

`get_settings` returns a flat `Dict[str, object]` keyed by slash-separated paths:

```python
settings = api_client.settings.get_settings(project_id)
# e.g. settings["calculationCommon/Checks/Shared/LimitPlasticStrain"] -> 0.05

# Search by keyword
settings = api_client.settings.get_settings(project_id, search="mesh")
```

Update a setting:

```python
api_client.settings.update_settings(project_id, {
    "calculationCommon/Checks/Shared/LimitPlasticStrain": 0.03
})
```

---

## Other useful APIs

### Load effects

```python
load_effects = api_client.load_effect.get_load_effects(project_id, conn_id)
```

### Materials

```python
steels   = api_client.material.get_steel_materials(project_id)
bolts    = api_client.material.get_bolt_grade_materials(project_id)
sections = api_client.material.get_cross_sections(project_id)
```

### Export to IOM/IFC

```python
iom_xml  = api_client.export.export_iom(project_id, conn_id)
ifc_data = api_client.export.export_ifc(project_id, conn_id)
```

### 3D scene data

```python
scene_json = api_client.presentation.get_data_scene3_d_text(project_id, conn_id)
```

---

## Complete working example

This is the pattern used in `main.py`:

```python
import glob, json, os
import ideastatica_connection_api.connection_api_service_attacher as connection_api_service_attacher

BASE_URL = "http://localhost:5000"

with connection_api_service_attacher.ConnectionApiServiceAttacher(BASE_URL).create_api_client() as api_client:
    api_client.project.open_project_from_filepath(r"inputs\myfile.ideaCon")
    project_id = api_client.project.active_project_id

    project_data = api_client.project.get_project_data(project_id)

    for conn in project_data.connections:
        conn_id = conn.id  # int

        # Skip calculation if already done
        results = api_client.calculation.get_results(project_id, [conn_id])
        if not (results and results[0] and results[0].check_res_summary is not None):
            api_client.calculation.calculate(project_id, [conn_id])

        # PDF report
        pdf_bytes = api_client.report.generate_pdf(project_id, conn_id)
        with open(f"reports/{conn.name}.pdf", "wb") as f:
            f.write(pdf_bytes)

        # Raw JSON data
        raw = api_client.calculation.get_raw_json_results(project_id, [conn_id])
        parsed = json.loads(raw[0])
        with open(f"data/{conn.name}.json", "w") as f:
            json.dump(parsed, f, indent=2)

    api_client.project.close_project(project_id)
```

---

## Gotchas

- **`ConCalculationParameter` does not exist** in the installed client. Pass `List[int]` directly to `calculate` and `get_raw_json_results`.
- **`generate_pdf` return type is annotated as `None`** but actually returns `bytes`. Open the file in `"wb"` mode.
- **All values are SI** — convert to display units in your own layer if needed.
- **Always close projects** — the server holds them in memory until explicitly closed or the session ends.
- **`active_project_id` is set automatically** after `open_project_from_filepath`. You do not need to parse the return value.
