statica-reporter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. statica_reporter-0.1.0/.github/workflows/ci.yml +56 -0
  2. statica_reporter-0.1.0/.github/workflows/release.yml +105 -0
  3. statica_reporter-0.1.0/.gitignore +43 -0
  4. statica_reporter-0.1.0/.python-version +1 -0
  5. statica_reporter-0.1.0/PKG-INFO +338 -0
  6. statica_reporter-0.1.0/README.md +325 -0
  7. statica_reporter-0.1.0/docs/superpowers/specs/2026-06-15-statica-reporter-tui-design.md +209 -0
  8. statica_reporter-0.1.0/docs/superpowers/specs/2026-06-15-statica-reporter-tui-plan.md +70 -0
  9. statica_reporter-0.1.0/docs/superpowers/specs/2026-06-16-white-label-docx-merge-design.md +251 -0
  10. statica_reporter-0.1.0/pyproject.toml +34 -0
  11. statica_reporter-0.1.0/scripts/build_template.py +24 -0
  12. statica_reporter-0.1.0/statica_reporter/__init__.py +0 -0
  13. statica_reporter-0.1.0/statica_reporter/cli.py +96 -0
  14. statica_reporter-0.1.0/statica_reporter/core/__init__.py +0 -0
  15. statica_reporter-0.1.0/statica_reporter/core/analysis.py +53 -0
  16. statica_reporter-0.1.0/statica_reporter/core/bound_report.py +79 -0
  17. statica_reporter-0.1.0/statica_reporter/core/checks.py +67 -0
  18. statica_reporter-0.1.0/statica_reporter/core/client.py +266 -0
  19. statica_reporter-0.1.0/statica_reporter/core/default_template.py +85 -0
  20. statica_reporter-0.1.0/statica_reporter/core/docx_merge.py +347 -0
  21. statica_reporter-0.1.0/statica_reporter/core/exports.py +257 -0
  22. statica_reporter-0.1.0/statica_reporter/core/models.py +105 -0
  23. statica_reporter-0.1.0/statica_reporter/core/rendering.py +173 -0
  24. statica_reporter-0.1.0/statica_reporter/core/service.py +110 -0
  25. statica_reporter-0.1.0/statica_reporter/core/summary_docx.py +152 -0
  26. statica_reporter-0.1.0/statica_reporter/core/units.py +49 -0
  27. statica_reporter-0.1.0/statica_reporter/tui/__init__.py +0 -0
  28. statica_reporter-0.1.0/statica_reporter/tui/app.py +182 -0
  29. statica_reporter-0.1.0/statica_reporter/tui/screens/__init__.py +0 -0
  30. statica_reporter-0.1.0/statica_reporter/tui/screens/browse.py +223 -0
  31. statica_reporter-0.1.0/statica_reporter/tui/screens/export_config.py +128 -0
  32. statica_reporter-0.1.0/statica_reporter/tui/screens/path_prompt.py +80 -0
  33. statica_reporter-0.1.0/statica_reporter/tui/screens/progress.py +72 -0
  34. statica_reporter-0.1.0/statica_reporter/tui/screens/prompt.py +92 -0
  35. statica_reporter-0.1.0/statica_reporter/tui/screens/report_builder.py +130 -0
  36. statica_reporter-0.1.0/statica_reporter/tui/screens/service_setup.py +85 -0
  37. statica_reporter-0.1.0/statica_reporter/tui/state.py +80 -0
  38. statica_reporter-0.1.0/statica_reporter/tui/widgets/__init__.py +0 -0
  39. statica_reporter-0.1.0/statica_reporter/tui/widgets/detail_pane.py +77 -0
  40. statica_reporter-0.1.0/statica_reporter/tui/widgets/project_tree.py +78 -0
  41. statica_reporter-0.1.0/statica_reporter/tui/widgets/selection_list.py +31 -0
  42. statica_reporter-0.1.0/statica_reporter/tui/workspace_actions.py +28 -0
  43. statica_reporter-0.1.0/statica_reporter/workspace.py +117 -0
  44. statica_reporter-0.1.0/tests/__init__.py +0 -0
  45. statica_reporter-0.1.0/tests/test_bound_report.py +156 -0
  46. statica_reporter-0.1.0/tests/test_exports.py +103 -0
  47. statica_reporter-0.1.0/tests/test_summary_docx.py +45 -0
  48. statica_reporter-0.1.0/tests/test_tui_smoke.py +50 -0
  49. statica_reporter-0.1.0/tests/test_units_checks.py +93 -0
  50. statica_reporter-0.1.0/tests/test_workspace.py +43 -0
  51. statica_reporter-0.1.0/uv.lock +1137 -0
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ # Cancel superseded runs on the same ref.
10
+ concurrency:
11
+ group: ci-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ lint:
16
+ name: Lint (ruff)
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v5
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Ruff lint
27
+ run: uvx ruff check --output-format=github .
28
+
29
+ - name: Ruff format check
30
+ run: uvx ruff format --check .
31
+
32
+ build:
33
+ name: Build distribution
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ with:
38
+ # Full history + tags so hatch-vcs can resolve a version.
39
+ fetch-depth: 0
40
+
41
+ - name: Install uv
42
+ uses: astral-sh/setup-uv@v5
43
+ with:
44
+ enable-cache: true
45
+
46
+ - name: Build sdist and wheel
47
+ run: uv build
48
+
49
+ - name: Check metadata
50
+ run: uvx twine check dist/*
51
+
52
+ - name: Upload build artifacts
53
+ uses: actions/upload-artifact@v4
54
+ with:
55
+ name: dist
56
+ path: dist/
@@ -0,0 +1,105 @@
1
+ name: Release
2
+
3
+ # Publish to PyPI when a version tag is pushed (e.g. `v1.2.3`).
4
+ # The version is derived from the tag by hatch-vcs, so the tag is the single
5
+ # source of truth — no version bump commit is needed.
6
+ on:
7
+ push:
8
+ tags:
9
+ - "v*"
10
+
11
+ jobs:
12
+ lint:
13
+ name: Lint (ruff)
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: Ruff lint
24
+ run: uvx ruff check --output-format=github .
25
+
26
+ - name: Ruff format check
27
+ run: uvx ruff format --check .
28
+
29
+ build:
30
+ name: Build distribution
31
+ needs: lint
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ with:
36
+ # Full history + tags so hatch-vcs resolves the version from the tag.
37
+ fetch-depth: 0
38
+
39
+ - name: Install uv
40
+ uses: astral-sh/setup-uv@v5
41
+ with:
42
+ enable-cache: true
43
+
44
+ - name: Build sdist and wheel
45
+ run: uv build
46
+
47
+ - name: Verify the built version matches the tag
48
+ run: |
49
+ tag="${GITHUB_REF_NAME#v}"
50
+ built=$(ls dist/*.whl | sed -E 's/.*statica_reporter-([^-]+)-.*/\1/')
51
+ echo "tag=$tag built=$built"
52
+ if [ "$tag" != "$built" ]; then
53
+ echo "::error::Built version '$built' does not match tag '$tag'." \
54
+ "Is the tag on a clean commit?"
55
+ exit 1
56
+ fi
57
+
58
+ - name: Check metadata
59
+ run: uvx twine check dist/*
60
+
61
+ - name: Upload build artifacts
62
+ uses: actions/upload-artifact@v4
63
+ with:
64
+ name: dist
65
+ path: dist/
66
+
67
+ publish:
68
+ name: Publish to PyPI
69
+ needs: build
70
+ runs-on: ubuntu-latest
71
+ # Must match the trusted publisher configured on PyPI.
72
+ environment:
73
+ name: pypi
74
+ url: https://pypi.org/p/statica-reporter
75
+ permissions:
76
+ # Required for Trusted Publishing (OIDC) — no API token needed.
77
+ id-token: write
78
+ steps:
79
+ - name: Download build artifacts
80
+ uses: actions/download-artifact@v4
81
+ with:
82
+ name: dist
83
+ path: dist/
84
+
85
+ - name: Publish to PyPI
86
+ uses: pypa/gh-action-pypi-publish@release/v1
87
+
88
+ github-release:
89
+ name: Create GitHub release
90
+ needs: publish
91
+ runs-on: ubuntu-latest
92
+ permissions:
93
+ contents: write
94
+ steps:
95
+ - name: Download build artifacts
96
+ uses: actions/download-artifact@v4
97
+ with:
98
+ name: dist
99
+ path: dist/
100
+
101
+ - name: Create release
102
+ uses: softprops/action-gh-release@v2
103
+ with:
104
+ generate_release_notes: true
105
+ files: dist/*
@@ -0,0 +1,43 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ # Virtual environments
6
+ .venv/
7
+ venv/
8
+ env/
9
+ ENV/
10
+
11
+ # Packaging
12
+ build/
13
+ dist/
14
+ *.egg-info/
15
+ .eggs/
16
+
17
+ # Tool caches
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .coverage
22
+ .coverage.*
23
+ htmlcov/
24
+
25
+ # Environment files
26
+ .env
27
+ .env.*
28
+
29
+ # IDEs and editors
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+
35
+ # OS files
36
+ .DS_Store
37
+ Thumbs.db
38
+
39
+ # Project folders
40
+ inputs/
41
+ reports/
42
+ data/
43
+ renders/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.4
2
+ Name: statica-reporter
3
+ Version: 0.1.0
4
+ Summary: TUI for batch IDEA StatiCa connection reporting and white-label export
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: ideastatica-connection-api
7
+ Requires-Dist: kaleido>=1.3.0
8
+ Requires-Dist: lxml>=5.0.0
9
+ Requires-Dist: plotly>=6.8.0
10
+ Requires-Dist: python-docx>=1.1.2
11
+ Requires-Dist: textual>=1.0.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # statica-reporter
15
+
16
+ A terminal UI (TUI) for batch IDEA StatiCa connection reporting. Browse multiple
17
+ `.ideaCon` projects and their connections in a tree, glance at per-connection
18
+ details, build an ordered report selection, and export in four formats.
19
+
20
+ ## Run
21
+
22
+ ```bash
23
+ # TUI (interactive)
24
+ uvx --from . statica-reporter # before publishing
25
+ # uvx statica-reporter # once published to PyPI
26
+
27
+ # Headless batch (preserves the original main.py workflow)
28
+ uvx --from . statica-reporter-batch --input-dir inputs --output-dir out --json
29
+ ```
30
+
31
+ Requires a running IDEA StatiCa Connection REST service. The TUI attaches to one
32
+ at `http://localhost:5000` by default; if none is reachable it offers to start a
33
+ detected local installation (or accept a custom URL).
34
+
35
+ ## Features
36
+
37
+ - **Browse**: tree of projects → connections with status glyphs; lazy two-pane
38
+ detail (status + per-category checks, design forces, geometry counts). Press
39
+ `Enter` to fetch, `c` to run CBFEM analysis.
40
+ - **Report builder**: move connections from an *Available* tree into an ordered
41
+ *Selected* list (`Space`/`Enter` add, `A` add all, `J`/`K` reorder, `d` remove).
42
+ - **Export** (combined by default, optional split-per-connection):
43
+ 1. StatiCa **PDF** (native multi-connection report, grouped per project)
44
+ 2. Connection **summary docx** (one table: ID, isometric view, forces, checks)
45
+ 3. **JSON** data dump (raw results)
46
+ 4. **Bound white-label docx** — all selected reports concatenated and run through
47
+ Pandoc with a blank reference document, so headings/tables map to stock Word
48
+ styles and inherit a company template on paste (with TOC + bookmarks).
49
+ - **Workspaces**: save/open named workspaces (added projects, selection, output
50
+ dir, export prefs) under `%APPDATA%/statica-reporter/workspaces`.
51
+
52
+ ## Architecture
53
+
54
+ - `statica_reporter/core/` — UI-free engine (service, client, analysis, exports,
55
+ rendering, summary_docx, bound_report). Fully unit-tested without a live service.
56
+ - `statica_reporter/tui/` — Textual app, screens, widgets.
57
+ - `statica_reporter/cli.py` — headless batch entry point.
58
+ - `statica_reporter/workspace.py` — named workspace persistence.
59
+
60
+ Run the tests with `uv run pytest`.
61
+
62
+ > The legacy root-level `main.py` and `report_generator.py` are superseded by the
63
+ > package (migrated into `core/`); they remain only for reference and can be removed.
64
+
65
+ ---
66
+
67
+ ## IDEA StatiCa Connection API — Developer Reference
68
+
69
+ 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.
70
+
71
+ ---
72
+
73
+ ## Setup
74
+
75
+ ### Install the client
76
+
77
+ ```bash
78
+ uv add ideastatica-connection-api
79
+ # or: pip install ideastatica-connection-api
80
+ ```
81
+
82
+ ### Prerequisites
83
+
84
+ 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`.
85
+
86
+ ---
87
+
88
+ ## Connecting
89
+
90
+ Use `ConnectionApiServiceAttacher` as a context manager. It handles session lifecycle automatically (client registration and cleanup on exit).
91
+
92
+ ```python
93
+ import ideastatica_connection_api.connection_api_service_attacher as connection_api_service_attacher
94
+
95
+ BASE_URL = "http://localhost:5000"
96
+
97
+ with connection_api_service_attacher.ConnectionApiServiceAttacher(BASE_URL).create_api_client() as api_client:
98
+ # all work happens here
99
+ pass
100
+ ```
101
+
102
+ The `api_client` object exposes every API group as an attribute:
103
+
104
+ | Attribute | Purpose |
105
+ | --- | --- |
106
+ | `api_client.project` | Open, close, query projects |
107
+ | `api_client.connection` | Query and modify connections |
108
+ | `api_client.calculation` | Run analysis, fetch results |
109
+ | `api_client.report` | Export PDF / Word / HTML |
110
+ | `api_client.load_effect` | Manage load cases |
111
+ | `api_client.material` | Steel, bolt, weld, concrete materials |
112
+ | `api_client.member` | Connection members |
113
+ | `api_client.operation` | Connection operations (welds, bolts, etc.) |
114
+ | `api_client.parameter` | Parametric values |
115
+ | `api_client.template` | Connection templates |
116
+ | `api_client.settings` | Project settings |
117
+ | `api_client.export` | IOM / IFC export |
118
+ | `api_client.presentation` | 3D scene data |
119
+
120
+ ---
121
+
122
+ ## Core workflow
123
+
124
+ ### 1. Open a project
125
+
126
+ ```python
127
+ api_client.project.open_project_from_filepath(r"C:\path\to\file.ideaCon")
128
+ project_id = api_client.project.active_project_id # str UUID, set automatically
129
+ ```
130
+
131
+ ### 2. List connections
132
+
133
+ ```python
134
+ project_data = api_client.project.get_project_data(project_id)
135
+ # project_data.connections -> list of ConConnection
136
+ for conn in project_data.connections:
137
+ print(conn.id, conn.name) # id is an int
138
+ ```
139
+
140
+ ### 3. Check if a connection is already analyzed
141
+
142
+ There is no explicit "status" field. Check by calling `get_results` and inspecting `check_res_summary`:
143
+
144
+ ```python
145
+ results = api_client.calculation.get_results(project_id, [conn_id])
146
+ already_analyzed = (
147
+ results
148
+ and results[0]
149
+ and results[0].check_res_summary is not None
150
+ )
151
+ ```
152
+
153
+ `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`.
154
+
155
+ ### 4. Run analysis
156
+
157
+ ```python
158
+ summaries = api_client.calculation.calculate(project_id, [conn_id])
159
+ # returns List[ConResultSummary]
160
+ # ConResultSummary fields: id, passed, result_summary
161
+ ```
162
+
163
+ `calculate` accepts a plain `List[int]` of connection IDs — pass multiple IDs to batch.
164
+
165
+ > **Note:** The official documentation shows a `ConCalculationParameter` object, but this class does not exist in the installed Python client. Pass a plain list directly.
166
+
167
+ ### 5. Fetch raw JSON results
168
+
169
+ ```python
170
+ raw_list = api_client.calculation.get_raw_json_results(project_id, [conn_id])
171
+ # returns List[str], one JSON string per connection ID
172
+ import json
173
+ data = json.loads(raw_list[0])
174
+ ```
175
+
176
+ Top-level keys in the returned object: `codeType`, `plates`, `platesDeformation`, `bolts`, `welds`, `anchors`, `concreteBlock`, `bucklingResults`, and others depending on connection type.
177
+
178
+ ### 6. Fetch structured results
179
+
180
+ ```python
181
+ results = api_client.calculation.get_results(project_id, [conn_id])
182
+ # returns List[ConnectionCheckRes]
183
+ summary = results[0].check_res_summary # ConResultSummary
184
+ plates = results[0].check_res_plate # list of CheckResPlate
185
+ welds = results[0].check_res_weld # list of CheckResWeld
186
+ bolts = results[0].check_res_bolt # list of CheckResBolt
187
+ ```
188
+
189
+ ### 7. Export reports
190
+
191
+ ```python
192
+ # PDF — returns bytes, write manually
193
+ pdf_bytes = api_client.report.generate_pdf(project_id, conn_id) # conn_id is int
194
+ with open("report.pdf", "wb") as f:
195
+ f.write(pdf_bytes)
196
+
197
+ # Word
198
+ docx_bytes = api_client.report.generate_word(project_id, conn_id)
199
+
200
+ # HTML zip
201
+ zip_bytes = api_client.report.generate_html_zip(project_id, conn_id)
202
+
203
+ # Batch PDF (all connections at once)
204
+ # Note: "mutliple" is a typo in the API itself — use it exactly as shown
205
+ pdf_bytes = api_client.report.generate_pdf_for_mutliple(project_id, [conn_id_1, conn_id_2])
206
+ ```
207
+
208
+ > **Note:** The return type annotation says `None` for `generate_pdf` — this is wrong. It returns `bytes`.
209
+
210
+ ### 8. Close the project
211
+
212
+ ```python
213
+ api_client.project.close_project(project_id)
214
+ ```
215
+
216
+ Always close when done. Unclosed projects remain in the server's memory.
217
+
218
+ ---
219
+
220
+ ## Units
221
+
222
+ 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.):
223
+
224
+ | Quantity | Unit |
225
+ | --- | --- |
226
+ | Length / dimension | m |
227
+ | Stress / pressure / strength | Pa |
228
+ | Force | N |
229
+ | Moment | N·m |
230
+
231
+ Example cross-check from a real result (`codeType: "american"`, material A992):
232
+
233
+ - `thickness: 0.04` → 40 mm plate ✓
234
+ - `materialFy: 344_740_000` → 344.7 MPa ≈ 50 ksi ✓
235
+ - `materialModulusOfElasticity: 200_000_000_000` → 200 GPa ✓
236
+
237
+ ---
238
+
239
+ ## Settings
240
+
241
+ `get_settings` returns a flat `Dict[str, object]` keyed by slash-separated paths:
242
+
243
+ ```python
244
+ settings = api_client.settings.get_settings(project_id)
245
+ # e.g. settings["calculationCommon/Checks/Shared/LimitPlasticStrain"] -> 0.05
246
+
247
+ # Search by keyword
248
+ settings = api_client.settings.get_settings(project_id, search="mesh")
249
+ ```
250
+
251
+ Update a setting:
252
+
253
+ ```python
254
+ api_client.settings.update_settings(project_id, {
255
+ "calculationCommon/Checks/Shared/LimitPlasticStrain": 0.03
256
+ })
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Other useful APIs
262
+
263
+ ### Load effects
264
+
265
+ ```python
266
+ load_effects = api_client.load_effect.get_load_effects(project_id, conn_id)
267
+ ```
268
+
269
+ ### Materials
270
+
271
+ ```python
272
+ steels = api_client.material.get_steel_materials(project_id)
273
+ bolts = api_client.material.get_bolt_grade_materials(project_id)
274
+ sections = api_client.material.get_cross_sections(project_id)
275
+ ```
276
+
277
+ ### Export to IOM/IFC
278
+
279
+ ```python
280
+ iom_xml = api_client.export.export_iom(project_id, conn_id)
281
+ ifc_data = api_client.export.export_ifc(project_id, conn_id)
282
+ ```
283
+
284
+ ### 3D scene data
285
+
286
+ ```python
287
+ scene_json = api_client.presentation.get_data_scene3_d_text(project_id, conn_id)
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Complete working example
293
+
294
+ This is the pattern used in `main.py`:
295
+
296
+ ```python
297
+ import glob, json, os
298
+ import ideastatica_connection_api.connection_api_service_attacher as connection_api_service_attacher
299
+
300
+ BASE_URL = "http://localhost:5000"
301
+
302
+ with connection_api_service_attacher.ConnectionApiServiceAttacher(BASE_URL).create_api_client() as api_client:
303
+ api_client.project.open_project_from_filepath(r"inputs\myfile.ideaCon")
304
+ project_id = api_client.project.active_project_id
305
+
306
+ project_data = api_client.project.get_project_data(project_id)
307
+
308
+ for conn in project_data.connections:
309
+ conn_id = conn.id # int
310
+
311
+ # Skip calculation if already done
312
+ results = api_client.calculation.get_results(project_id, [conn_id])
313
+ if not (results and results[0] and results[0].check_res_summary is not None):
314
+ api_client.calculation.calculate(project_id, [conn_id])
315
+
316
+ # PDF report
317
+ pdf_bytes = api_client.report.generate_pdf(project_id, conn_id)
318
+ with open(f"reports/{conn.name}.pdf", "wb") as f:
319
+ f.write(pdf_bytes)
320
+
321
+ # Raw JSON data
322
+ raw = api_client.calculation.get_raw_json_results(project_id, [conn_id])
323
+ parsed = json.loads(raw[0])
324
+ with open(f"data/{conn.name}.json", "w") as f:
325
+ json.dump(parsed, f, indent=2)
326
+
327
+ api_client.project.close_project(project_id)
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Gotchas
333
+
334
+ - **`ConCalculationParameter` does not exist** in the installed client. Pass `List[int]` directly to `calculate` and `get_raw_json_results`.
335
+ - **`generate_pdf` return type is annotated as `None`** but actually returns `bytes`. Open the file in `"wb"` mode.
336
+ - **All values are SI** — convert to display units in your own layer if needed.
337
+ - **Always close projects** — the server holds them in memory until explicitly closed or the session ends.
338
+ - **`active_project_id` is set automatically** after `open_project_from_filepath`. You do not need to parse the return value.