boarddata 3.0.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 (52) hide show
  1. boarddata-3.0.0/PKG-INFO +111 -0
  2. boarddata-3.0.0/README.md +87 -0
  3. boarddata-3.0.0/__init__.py +18 -0
  4. boarddata-3.0.0/_assemblies.py +400 -0
  5. boarddata-3.0.0/_auditors.py +286 -0
  6. boarddata-3.0.0/_base.py +217 -0
  7. boarddata-3.0.0/_comex.py +522 -0
  8. boarddata-3.0.0/_companies.py +349 -0
  9. boarddata-3.0.0/_directors.py +513 -0
  10. boarddata-3.0.0/_documents.py +152 -0
  11. boarddata-3.0.0/_esg.py +271 -0
  12. boarddata-3.0.0/_persons.py +308 -0
  13. boarddata-3.0.0/_sentinel.py +143 -0
  14. boarddata-3.0.0/_utilities.py +157 -0
  15. boarddata-3.0.0/boarddata.egg-info/PKG-INFO +111 -0
  16. boarddata-3.0.0/boarddata.egg-info/SOURCES.txt +66 -0
  17. boarddata-3.0.0/boarddata.egg-info/dependency_links.txt +1 -0
  18. boarddata-3.0.0/boarddata.egg-info/requires.txt +9 -0
  19. boarddata-3.0.0/boarddata.egg-info/top_level.txt +1 -0
  20. boarddata-3.0.0/cache.py +39 -0
  21. boarddata-3.0.0/client.py +42 -0
  22. boarddata-3.0.0/errors.py +14 -0
  23. boarddata-3.0.0/py.typed +0 -0
  24. boarddata-3.0.0/pyproject.toml +61 -0
  25. boarddata-3.0.0/setup.cfg +4 -0
  26. boarddata-3.0.0/tests/__init__.py +0 -0
  27. boarddata-3.0.0/tests/conftest.py +19 -0
  28. boarddata-3.0.0/tests/test_assemblies.py +54 -0
  29. boarddata-3.0.0/tests/test_auditors.py +51 -0
  30. boarddata-3.0.0/tests/test_base.py +35 -0
  31. boarddata-3.0.0/tests/test_build_payload.py +25 -0
  32. boarddata-3.0.0/tests/test_cache.py +35 -0
  33. boarddata-3.0.0/tests/test_comex.py +66 -0
  34. boarddata-3.0.0/tests/test_companies.py +110 -0
  35. boarddata-3.0.0/tests/test_config.py +48 -0
  36. boarddata-3.0.0/tests/test_directors.py +63 -0
  37. boarddata-3.0.0/tests/test_documents.py +40 -0
  38. boarddata-3.0.0/tests/test_esg.py +51 -0
  39. boarddata-3.0.0/tests/test_persons.py +65 -0
  40. boarddata-3.0.0/tests/test_sentinel.py +51 -0
  41. boarddata-3.0.0/tests/test_utilities.py +74 -0
  42. boarddata-3.0.0/types/__init__.py +221 -0
  43. boarddata-3.0.0/types/assemblies.py +395 -0
  44. boarddata-3.0.0/types/auditors.py +114 -0
  45. boarddata-3.0.0/types/comex.py +314 -0
  46. boarddata-3.0.0/types/companies.py +128 -0
  47. boarddata-3.0.0/types/core.py +85 -0
  48. boarddata-3.0.0/types/directors.py +179 -0
  49. boarddata-3.0.0/types/documents.py +57 -0
  50. boarddata-3.0.0/types/esg.py +180 -0
  51. boarddata-3.0.0/types/persons.py +106 -0
  52. boarddata-3.0.0/types/sentinel.py +84 -0
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: boarddata
3
+ Version: 3.0.0
4
+ Summary: Python SDK for the BoardData V2 REST API
5
+ License: MIT
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.28
19
+ Requires-Dist: typing_extensions>=4.0; python_version < "3.11"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == "dev"
22
+ Requires-Dist: ruff; extra == "dev"
23
+ Requires-Dist: mypy; extra == "dev"
24
+
25
+ # boarddata
26
+
27
+ Python SDK for the BoardData V2 REST API.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install boarddata
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ from boarddata import BoardDataClient
39
+
40
+ # From environment variables
41
+ client = BoardDataClient.from_env()
42
+
43
+ # Or explicit configuration
44
+ client = BoardDataClient(
45
+ base_url="https://api.boarddata.scalens.com",
46
+ auth0_domain="your-tenant.auth0.com",
47
+ client_id="your-client-id",
48
+ client_secret="your-client-secret",
49
+ )
50
+
51
+ # List companies
52
+ companies = client.list_companies(country="FR", page_size=10)
53
+
54
+ # Get a specific company
55
+ company = client.get_company("company-uuid")
56
+
57
+ # Create or update a company
58
+ result = client.upsert_company("FR0000120271", "TotalEnergies", country="FR")
59
+ print(result["action"]) # "created" or "updated"
60
+ print(result["id"]) # company UUID
61
+ ```
62
+
63
+ ## Environment Variables
64
+
65
+ | Variable | Description |
66
+ |---|---|
67
+ | `BOARDDATA_BACKEND_URL` | Base URL of the BoardData API |
68
+ | `BOARDDATA_AUTH0_DOMAIN` | Auth0 tenant domain |
69
+ | `BOARDDATA_AUTH0_CLIENT_ID` | Auth0 client ID |
70
+ | `BOARDDATA_AUTH0_CLIENT_SECRET` | Auth0 client secret |
71
+
72
+ ## Token Caching
73
+
74
+ ```python
75
+ from boarddata import BoardDataClient, FileTokenCache
76
+
77
+ # File-based cache
78
+ client = BoardDataClient.from_env(token_cache="~/.boarddata/token.json")
79
+
80
+ # Or with explicit cache object
81
+ cache = FileTokenCache("~/.boarddata/token.json")
82
+ client = BoardDataClient.from_env(token_cache=cache)
83
+ ```
84
+
85
+ ## Pagination
86
+
87
+ ```python
88
+ # Auto-paginate any list endpoint
89
+ all_companies = client.paginate(client.list_companies, country="FR")
90
+
91
+ # Limit pages
92
+ first_100 = client.paginate(client.list_companies, max_pages=4, page_size=25)
93
+ ```
94
+
95
+ ## Available Domains
96
+
97
+ - **Companies** — `list_companies`, `get_company`, `create_company`, `update_company`, `delete_company`, `upsert_company`
98
+ - **Persons** — `list_persons`, `get_person`, `create_person`, `update_person`, `delete_person`, `upsert_person`
99
+ - **Directors** — `list_directors`, `get_director`, `create_director`, `update_director`, `delete_director`, `upsert_director`
100
+ - **Comex** — `list_comex`, `get_comex`, `create_comex`, `update_comex`, `delete_comex`, `upsert_comex`
101
+ - **Auditors** — `list_auditors`, `get_auditor`, `create_auditor`, `update_auditor`, `delete_auditor`
102
+ - **Documents** — `list_documents`, `get_document`, `create_document`, `update_document`, `delete_document`
103
+ - **Assemblies** — `list_assemblies`, `get_assembly`, `create_assembly`, `update_assembly`, `delete_assembly`
104
+ - **Resolutions** — `list_resolutions`, `get_resolution`, `update_resolution`, `delete_resolution`
105
+ - **ESG** — `list_iros`, `create_iro`, `list_transition_plans`, `create_transition_plan`, `list_esg_benchmark`
106
+ - **Sentinel** — `list_analyses`, `create_analysis`, `retry_press_extraction`, `update_press_article`
107
+ - **Utilities** — `search`, `query_assistant`, `get_field_sources`, `paginate`
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,87 @@
1
+ # boarddata
2
+
3
+ Python SDK for the BoardData V2 REST API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install boarddata
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from boarddata import BoardDataClient
15
+
16
+ # From environment variables
17
+ client = BoardDataClient.from_env()
18
+
19
+ # Or explicit configuration
20
+ client = BoardDataClient(
21
+ base_url="https://api.boarddata.scalens.com",
22
+ auth0_domain="your-tenant.auth0.com",
23
+ client_id="your-client-id",
24
+ client_secret="your-client-secret",
25
+ )
26
+
27
+ # List companies
28
+ companies = client.list_companies(country="FR", page_size=10)
29
+
30
+ # Get a specific company
31
+ company = client.get_company("company-uuid")
32
+
33
+ # Create or update a company
34
+ result = client.upsert_company("FR0000120271", "TotalEnergies", country="FR")
35
+ print(result["action"]) # "created" or "updated"
36
+ print(result["id"]) # company UUID
37
+ ```
38
+
39
+ ## Environment Variables
40
+
41
+ | Variable | Description |
42
+ |---|---|
43
+ | `BOARDDATA_BACKEND_URL` | Base URL of the BoardData API |
44
+ | `BOARDDATA_AUTH0_DOMAIN` | Auth0 tenant domain |
45
+ | `BOARDDATA_AUTH0_CLIENT_ID` | Auth0 client ID |
46
+ | `BOARDDATA_AUTH0_CLIENT_SECRET` | Auth0 client secret |
47
+
48
+ ## Token Caching
49
+
50
+ ```python
51
+ from boarddata import BoardDataClient, FileTokenCache
52
+
53
+ # File-based cache
54
+ client = BoardDataClient.from_env(token_cache="~/.boarddata/token.json")
55
+
56
+ # Or with explicit cache object
57
+ cache = FileTokenCache("~/.boarddata/token.json")
58
+ client = BoardDataClient.from_env(token_cache=cache)
59
+ ```
60
+
61
+ ## Pagination
62
+
63
+ ```python
64
+ # Auto-paginate any list endpoint
65
+ all_companies = client.paginate(client.list_companies, country="FR")
66
+
67
+ # Limit pages
68
+ first_100 = client.paginate(client.list_companies, max_pages=4, page_size=25)
69
+ ```
70
+
71
+ ## Available Domains
72
+
73
+ - **Companies** — `list_companies`, `get_company`, `create_company`, `update_company`, `delete_company`, `upsert_company`
74
+ - **Persons** — `list_persons`, `get_person`, `create_person`, `update_person`, `delete_person`, `upsert_person`
75
+ - **Directors** — `list_directors`, `get_director`, `create_director`, `update_director`, `delete_director`, `upsert_director`
76
+ - **Comex** — `list_comex`, `get_comex`, `create_comex`, `update_comex`, `delete_comex`, `upsert_comex`
77
+ - **Auditors** — `list_auditors`, `get_auditor`, `create_auditor`, `update_auditor`, `delete_auditor`
78
+ - **Documents** — `list_documents`, `get_document`, `create_document`, `update_document`, `delete_document`
79
+ - **Assemblies** — `list_assemblies`, `get_assembly`, `create_assembly`, `update_assembly`, `delete_assembly`
80
+ - **Resolutions** — `list_resolutions`, `get_resolution`, `update_resolution`, `delete_resolution`
81
+ - **ESG** — `list_iros`, `create_iro`, `list_transition_plans`, `create_transition_plan`, `list_esg_benchmark`
82
+ - **Sentinel** — `list_analyses`, `create_analysis`, `retry_press_extraction`, `update_press_article`
83
+ - **Utilities** — `search`, `query_assistant`, `get_field_sources`, `paginate`
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,18 @@
1
+ """BoardData API Python SDK."""
2
+
3
+ from .cache import FileTokenCache
4
+ from ._base import Base, TokenCache
5
+ from .client import BoardDataClient
6
+ from .errors import BoardDataError
7
+
8
+ from . import types as types # noqa: F401 — makes types accessible as boarddata.types
9
+
10
+ __version__ = "3.0.0"
11
+ __all__ = [
12
+ "Base",
13
+ "BoardDataClient",
14
+ "BoardDataError",
15
+ "FileTokenCache",
16
+ "TokenCache",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,400 @@
1
+ """Assembly, resolution, presentation, and question methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .types.assemblies import (
8
+ AssemblyDetail,
9
+ CreatePresentationPayload,
10
+ CreateQuestionPayload,
11
+ CreateResolutionPayload,
12
+ PresentationItem,
13
+ QuestionItem,
14
+ ResolutionDetail,
15
+ UpdatePresentationPayload,
16
+ UpdateQuestionPayload,
17
+ UpdateResolutionPayload,
18
+ )
19
+ from .types.core import FieldSourcePayload, PaginatedResponse
20
+
21
+
22
+ class AssemblyMixin:
23
+ """Assembly, resolution, presentation, and question API methods."""
24
+
25
+ # ------------------------------------------------------------------
26
+ # Assemblies
27
+ # ------------------------------------------------------------------
28
+
29
+ def list_assemblies(self, **params: Any) -> PaginatedResponse:
30
+ """List assemblies.
31
+
32
+ Args:
33
+ company: Filter by company UUID.
34
+ assembly_type: Filter by assembly type (``"OGM"``, ``"EGM"``, ``"COGM"``).
35
+ fiscal_year: Filter by fiscal year.
36
+ page: Page number.
37
+ page_size: Results per page.
38
+
39
+ Returns:
40
+ Paginated response with assembly results.
41
+
42
+ Raises:
43
+ BoardDataError: On non-2xx API response.
44
+ """
45
+ return self._get("assemblies/", **params) # type: ignore[attr-defined]
46
+
47
+ def get_assembly(self, uid: str) -> AssemblyDetail:
48
+ """Retrieve an assembly by UUID.
49
+
50
+ Args:
51
+ uid: Assembly UUID.
52
+
53
+ Returns:
54
+ AssemblyDetail with nested resolutions, presentations, questions.
55
+
56
+ Raises:
57
+ BoardDataError: On non-2xx API response.
58
+ """
59
+ return self._get(f"assemblies/{uid}/") # type: ignore[attr-defined]
60
+
61
+ def create_assembly(
62
+ self,
63
+ company: str,
64
+ assembly_date: str,
65
+ assembly_type: str,
66
+ fiscal_year: int,
67
+ *,
68
+ postponed_date: str | None = None,
69
+ date_not_certified: bool | None = None,
70
+ motivation: str | None = None,
71
+ agenda: str | None = None,
72
+ report: str | None = None,
73
+ report_purpose: str | None = None,
74
+ quorum: str | None = None,
75
+ attendee_count: int | None = None,
76
+ includes_abstention: bool | None = None,
77
+ present_count: int | None = None,
78
+ remote_count: int | None = None,
79
+ questions_count: int | None = None,
80
+ written_questions_count: int | None = None,
81
+ questions_duration: str | None = None,
82
+ duration: str | None = None,
83
+ resolution_count: int | None = None,
84
+ format: str | None = None,
85
+ is_virtual: bool | None = None,
86
+ is_in_person: bool | None = None,
87
+ is_hybrid: bool | None = None,
88
+ has_live_voting: bool | None = None,
89
+ has_replay: bool | None = None,
90
+ has_live_qa: bool | None = None,
91
+ has_video: bool | None = None,
92
+ has_audio: bool | None = None,
93
+ has_simultaneous_translation: bool | None = None,
94
+ closed_doors: bool | None = None,
95
+ balo_published: bool | None = None,
96
+ dividend_affected: bool | None = None,
97
+ field_sources: list[FieldSourcePayload] | None = None,
98
+ ) -> AssemblyDetail:
99
+ """Create an assembly.
100
+
101
+ Args:
102
+ company: Company UUID.
103
+ assembly_date: Assembly date (``"YYYY-MM-DD"``).
104
+ assembly_type: Type of assembly.
105
+ fiscal_year: Fiscal year.
106
+ postponed_date: Postponed date if applicable.
107
+ date_not_certified: Whether the date is not certified.
108
+ motivation: Assembly motivation text.
109
+ agenda: Assembly agenda text.
110
+ report: Assembly report text.
111
+ report_purpose: Report purpose text.
112
+ quorum: Quorum percentage (decimal as string).
113
+ attendee_count: Total attendee count.
114
+ includes_abstention: Whether vote counts include abstentions.
115
+ present_count: Number physically present.
116
+ remote_count: Number attending remotely.
117
+ questions_count: Number of questions asked.
118
+ written_questions_count: Number of written questions.
119
+ questions_duration: Duration of Q&A (``"HH:MM:SS"``).
120
+ duration: Total assembly duration (``"HH:MM:SS"``).
121
+ resolution_count: Number of resolutions.
122
+ format: Assembly format.
123
+ is_virtual: Whether assembly was virtual.
124
+ is_in_person: Whether assembly was in person.
125
+ is_hybrid: Whether assembly was hybrid.
126
+ has_live_voting: Whether live voting was available.
127
+ has_replay: Whether replay is available.
128
+ has_live_qa: Whether live Q&A was available.
129
+ has_video: Whether video was available.
130
+ has_audio: Whether audio was available.
131
+ has_simultaneous_translation: Whether simultaneous translation was available.
132
+ closed_doors: Whether assembly was behind closed doors.
133
+ balo_published: Whether BALO was published.
134
+ dividend_affected: Whether dividend was affected.
135
+ field_sources: Document provenance for fields.
136
+
137
+ Returns:
138
+ Created AssemblyDetail.
139
+
140
+ Raises:
141
+ BoardDataError: On non-2xx API response.
142
+ """
143
+ payload = self._build_payload( # type: ignore[attr-defined]
144
+ company=company,
145
+ assembly_date=assembly_date,
146
+ assembly_type=assembly_type,
147
+ fiscal_year=fiscal_year,
148
+ postponed_date=postponed_date,
149
+ date_not_certified=date_not_certified,
150
+ motivation=motivation,
151
+ agenda=agenda,
152
+ report=report,
153
+ report_purpose=report_purpose,
154
+ quorum=quorum,
155
+ attendee_count=attendee_count,
156
+ includes_abstention=includes_abstention,
157
+ present_count=present_count,
158
+ remote_count=remote_count,
159
+ questions_count=questions_count,
160
+ written_questions_count=written_questions_count,
161
+ questions_duration=questions_duration,
162
+ duration=duration,
163
+ resolution_count=resolution_count,
164
+ format=format,
165
+ is_virtual=is_virtual,
166
+ is_in_person=is_in_person,
167
+ is_hybrid=is_hybrid,
168
+ has_live_voting=has_live_voting,
169
+ has_replay=has_replay,
170
+ has_live_qa=has_live_qa,
171
+ has_video=has_video,
172
+ has_audio=has_audio,
173
+ has_simultaneous_translation=has_simultaneous_translation,
174
+ closed_doors=closed_doors,
175
+ balo_published=balo_published,
176
+ dividend_affected=dividend_affected,
177
+ field_sources=field_sources,
178
+ )
179
+ return self._post("assemblies/", payload) # type: ignore[attr-defined]
180
+
181
+ def update_assembly(
182
+ self,
183
+ uid: str,
184
+ *,
185
+ assembly_date: str | None = None,
186
+ assembly_type: str | None = None,
187
+ fiscal_year: int | None = None,
188
+ postponed_date: str | None = None,
189
+ date_not_certified: bool | None = None,
190
+ motivation: str | None = None,
191
+ agenda: str | None = None,
192
+ report: str | None = None,
193
+ report_purpose: str | None = None,
194
+ quorum: str | None = None,
195
+ attendee_count: int | None = None,
196
+ includes_abstention: bool | None = None,
197
+ present_count: int | None = None,
198
+ remote_count: int | None = None,
199
+ questions_count: int | None = None,
200
+ written_questions_count: int | None = None,
201
+ questions_duration: str | None = None,
202
+ duration: str | None = None,
203
+ resolution_count: int | None = None,
204
+ format: str | None = None,
205
+ is_virtual: bool | None = None,
206
+ is_in_person: bool | None = None,
207
+ is_hybrid: bool | None = None,
208
+ has_live_voting: bool | None = None,
209
+ has_replay: bool | None = None,
210
+ has_live_qa: bool | None = None,
211
+ has_video: bool | None = None,
212
+ has_audio: bool | None = None,
213
+ has_simultaneous_translation: bool | None = None,
214
+ closed_doors: bool | None = None,
215
+ balo_published: bool | None = None,
216
+ dividend_affected: bool | None = None,
217
+ ) -> AssemblyDetail:
218
+ """Partial update an assembly.
219
+
220
+ Args:
221
+ uid: Assembly UUID.
222
+ (All other args same as create_assembly, all optional.)
223
+
224
+ Returns:
225
+ Updated AssemblyDetail.
226
+
227
+ Raises:
228
+ BoardDataError: On non-2xx API response.
229
+ """
230
+ payload = self._build_payload( # type: ignore[attr-defined]
231
+ assembly_date=assembly_date,
232
+ assembly_type=assembly_type,
233
+ fiscal_year=fiscal_year,
234
+ postponed_date=postponed_date,
235
+ date_not_certified=date_not_certified,
236
+ motivation=motivation,
237
+ agenda=agenda,
238
+ report=report,
239
+ report_purpose=report_purpose,
240
+ quorum=quorum,
241
+ attendee_count=attendee_count,
242
+ includes_abstention=includes_abstention,
243
+ present_count=present_count,
244
+ remote_count=remote_count,
245
+ questions_count=questions_count,
246
+ written_questions_count=written_questions_count,
247
+ questions_duration=questions_duration,
248
+ duration=duration,
249
+ resolution_count=resolution_count,
250
+ format=format,
251
+ is_virtual=is_virtual,
252
+ is_in_person=is_in_person,
253
+ is_hybrid=is_hybrid,
254
+ has_live_voting=has_live_voting,
255
+ has_replay=has_replay,
256
+ has_live_qa=has_live_qa,
257
+ has_video=has_video,
258
+ has_audio=has_audio,
259
+ has_simultaneous_translation=has_simultaneous_translation,
260
+ closed_doors=closed_doors,
261
+ balo_published=balo_published,
262
+ dividend_affected=dividend_affected,
263
+ )
264
+ return self._patch(f"assemblies/{uid}/", payload) # type: ignore[attr-defined]
265
+
266
+ def delete_assembly(self, uid: str) -> None:
267
+ """Soft-delete an assembly."""
268
+ self._delete(f"assemblies/{uid}/") # type: ignore[attr-defined]
269
+
270
+ def get_assembly_stats(self, **params: Any) -> Any:
271
+ """Get assembly statistics."""
272
+ return self._get("assemblies/stats/", **params) # type: ignore[attr-defined]
273
+
274
+ # -- Presentations --
275
+
276
+ def list_presentations(self, assembly_uid: str, **params: Any) -> PaginatedResponse:
277
+ """List presentations for an assembly."""
278
+ return self._get(f"assemblies/{assembly_uid}/presentations/", **params) # type: ignore[attr-defined]
279
+
280
+ def create_presentations(
281
+ self, assembly_uid: str, data: list[CreatePresentationPayload],
282
+ ) -> list[PresentationItem]:
283
+ """Batch-create presentations for an assembly."""
284
+ return self._post(f"assemblies/{assembly_uid}/presentations/", data) # type: ignore[attr-defined]
285
+
286
+ def update_presentation(
287
+ self, assembly_uid: str, pk: str, data: UpdatePresentationPayload,
288
+ ) -> PresentationItem:
289
+ """Partial update a presentation."""
290
+ return self._patch(f"assemblies/{assembly_uid}/presentations/{pk}/", data) # type: ignore[attr-defined]
291
+
292
+ def delete_presentation(self, assembly_uid: str, pk: str) -> None:
293
+ """Delete a presentation."""
294
+ self._delete(f"assemblies/{assembly_uid}/presentations/{pk}/") # type: ignore[attr-defined]
295
+
296
+ # -- Questions --
297
+
298
+ def list_questions(self, assembly_uid: str, **params: Any) -> PaginatedResponse:
299
+ """List questions for an assembly."""
300
+ return self._get(f"assemblies/{assembly_uid}/questions/", **params) # type: ignore[attr-defined]
301
+
302
+ def create_questions(
303
+ self, assembly_uid: str, data: list[CreateQuestionPayload],
304
+ ) -> list[QuestionItem]:
305
+ """Batch-create questions for an assembly."""
306
+ return self._post(f"assemblies/{assembly_uid}/questions/", data) # type: ignore[attr-defined]
307
+
308
+ def update_question(
309
+ self, assembly_uid: str, pk: str, data: UpdateQuestionPayload,
310
+ ) -> QuestionItem:
311
+ """Partial update a question."""
312
+ return self._patch(f"assemblies/{assembly_uid}/questions/{pk}/", data) # type: ignore[attr-defined]
313
+
314
+ def delete_question(self, assembly_uid: str, pk: str) -> None:
315
+ """Delete a question."""
316
+ self._delete(f"assemblies/{assembly_uid}/questions/{pk}/") # type: ignore[attr-defined]
317
+
318
+ # -- Resolutions --
319
+
320
+ def list_resolutions(self, **params: Any) -> PaginatedResponse:
321
+ """List resolutions (top-level, with filters)."""
322
+ return self._get("resolutions/", **params) # type: ignore[attr-defined]
323
+
324
+ def get_resolution(self, uid: str) -> ResolutionDetail:
325
+ """Retrieve a resolution by UUID."""
326
+ return self._get(f"resolutions/{uid}/") # type: ignore[attr-defined]
327
+
328
+ def update_resolution(
329
+ self,
330
+ uid: str,
331
+ *,
332
+ number_or_letter: str | None = None,
333
+ resolution_type: int | None = None,
334
+ description: str | None = None,
335
+ status: str | None = None,
336
+ approval_rate: str | None = None,
337
+ votes_for: int | None = None,
338
+ votes_against: int | None = None,
339
+ abstentions: int | None = None,
340
+ is_shareholder_proposal: bool | None = None,
341
+ is_dissenting: bool | None = None,
342
+ is_agreed: bool | None = None,
343
+ ) -> ResolutionDetail:
344
+ """Partial update a resolution (top-level endpoint)."""
345
+ payload = self._build_payload( # type: ignore[attr-defined]
346
+ number_or_letter=number_or_letter,
347
+ resolution_type=resolution_type,
348
+ description=description,
349
+ status=status,
350
+ approval_rate=approval_rate,
351
+ votes_for=votes_for,
352
+ votes_against=votes_against,
353
+ abstentions=abstentions,
354
+ is_shareholder_proposal=is_shareholder_proposal,
355
+ is_dissenting=is_dissenting,
356
+ is_agreed=is_agreed,
357
+ )
358
+ return self._patch(f"resolutions/{uid}/", payload) # type: ignore[attr-defined]
359
+
360
+ def delete_resolution(self, uid: str) -> None:
361
+ """Delete a resolution (top-level endpoint)."""
362
+ self._delete(f"resolutions/{uid}/") # type: ignore[attr-defined]
363
+
364
+ def get_resolution_typestats(self, **params: Any) -> Any:
365
+ """Get per-resolution-type average approval rate and count."""
366
+ return self._get("resolutions/typestats/", **params) # type: ignore[attr-defined]
367
+
368
+ def list_assembly_resolutions(self, assembly_uid: str, **params: Any) -> PaginatedResponse:
369
+ """List resolutions nested under an assembly."""
370
+ return self._get(f"assemblies/{assembly_uid}/resolutions/", **params) # type: ignore[attr-defined]
371
+
372
+ def create_assembly_resolution(
373
+ self, assembly_uid: str, data: list[CreateResolutionPayload],
374
+ ) -> list[ResolutionDetail]:
375
+ """Create (batch upsert) resolutions under an assembly."""
376
+ return self._post(f"assemblies/{assembly_uid}/resolutions/", data) # type: ignore[attr-defined]
377
+
378
+ def update_assembly_resolution(
379
+ self, assembly_uid: str, pk: str, data: UpdateResolutionPayload,
380
+ ) -> ResolutionDetail:
381
+ """Partial update a resolution under an assembly."""
382
+ return self._patch(f"assemblies/{assembly_uid}/resolutions/{pk}/", data) # type: ignore[attr-defined]
383
+
384
+ def delete_assembly_resolution(self, assembly_uid: str, pk: str) -> None:
385
+ """Delete a resolution under an assembly."""
386
+ self._delete(f"assemblies/{assembly_uid}/resolutions/{pk}/") # type: ignore[attr-defined]
387
+
388
+ # -- Lookup tables --
389
+
390
+ def list_resolution_types(self, **params: Any) -> PaginatedResponse:
391
+ """List resolution types."""
392
+ return self._get("resolution-types/", **params) # type: ignore[attr-defined]
393
+
394
+ def list_presentation_categories(self, **params: Any) -> PaginatedResponse:
395
+ """List presentation categories."""
396
+ return self._get("presentation-categories/", **params) # type: ignore[attr-defined]
397
+
398
+ def list_question_themes(self, **params: Any) -> PaginatedResponse:
399
+ """List question themes."""
400
+ return self._get("question-themes/", **params) # type: ignore[attr-defined]