aggregate_api 1.0.0__py3-none-any.whl

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 (63) hide show
  1. aggregate_api/__init__.py +41 -0
  2. aggregate_api/__main__.py +154 -0
  3. aggregate_api/app.py +206 -0
  4. aggregate_api/audit.py +395 -0
  5. aggregate_api/bounds.py +331 -0
  6. aggregate_api/cache.py +319 -0
  7. aggregate_api/capability.py +823 -0
  8. aggregate_api/completion.py +219 -0
  9. aggregate_api/config.py +363 -0
  10. aggregate_api/cors.py +61 -0
  11. aggregate_api/examples.py +620 -0
  12. aggregate_api/layer_pricing.py +840 -0
  13. aggregate_api/library.py +94 -0
  14. aggregate_api/library_notes.py +96 -0
  15. aggregate_api/models.py +1407 -0
  16. aggregate_api/net.py +281 -0
  17. aggregate_api/pnl.py +101 -0
  18. aggregate_api/pricing.py +778 -0
  19. aggregate_api/resources.py +257 -0
  20. aggregate_api/routes/__init__.py +8 -0
  21. aggregate_api/routes/decl.py +327 -0
  22. aggregate_api/routes/examples.py +82 -0
  23. aggregate_api/routes/meta.py +282 -0
  24. aggregate_api/routes/objects.py +4119 -0
  25. aggregate_api/routes/status.py +466 -0
  26. aggregate_api/serializers.py +565 -0
  27. aggregate_api/sessions.py +353 -0
  28. aggregate_api/static/aggregate-api-logo-512.png +0 -0
  29. aggregate_api/static/aggregate-api-logo.png +0 -0
  30. aggregate_api/static/aggregate-api-trim.png +0 -0
  31. aggregate_api/static/android-chrome-192x192.png +0 -0
  32. aggregate_api/static/android-chrome-512x512.png +0 -0
  33. aggregate_api/static/apple-touch-icon.png +0 -0
  34. aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
  35. aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  36. aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
  37. aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
  38. aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
  39. aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
  40. aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
  41. aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
  42. aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
  43. aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
  44. aggregate_api/static/assets/main-CmoEiPit.js +9 -0
  45. aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
  46. aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
  47. aggregate_api/static/favicon-16x16.png +0 -0
  48. aggregate_api/static/favicon-32x32.png +0 -0
  49. aggregate_api/static/favicon.ico +0 -0
  50. aggregate_api/static/index.html +912 -0
  51. aggregate_api/static/lite.html +83 -0
  52. aggregate_api/static/logo.png +0 -0
  53. aggregate_api/static/site.webmanifest +14 -0
  54. aggregate_api/static/sw.js +78 -0
  55. aggregate_api/status.py +536 -0
  56. aggregate_api/status_page.html +546 -0
  57. aggregate_api/tables.py +316 -0
  58. aggregate_api-1.0.0.dist-info/METADATA +187 -0
  59. aggregate_api-1.0.0.dist-info/RECORD +63 -0
  60. aggregate_api-1.0.0.dist-info/WHEEL +5 -0
  61. aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
  62. aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
  63. aggregate_api-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1407 @@
1
+ """Pydantic v2 schemas for the api request and response bodies.
2
+
3
+ Each endpoint takes / returns one of these dataclass-like models.
4
+ FastAPI validates incoming JSON against the request model and
5
+ serializes outgoing responses through the declared return-type
6
+ model -- the trip through OpenAPI is automatic.
7
+
8
+ Flask users: think Marshmallow / pydantic-flask, but tighter --
9
+ the model *is* the function parameter type, not a separate
10
+ schema you call ``schema.load(request.json)`` on.
11
+
12
+ Conventions
13
+ -----------
14
+
15
+ * Field names are ``snake_case``.
16
+ * All response models opt into ``ConfigDict(extra="forbid")`` so
17
+ the client can rely on the documented field set -- a typo in
18
+ the server code triggers a serialization error instead of
19
+ silently shipping a malformed payload.
20
+ * The plan calls for ``InfoResponse.info`` to be a ``dict``,
21
+ but :attr:`aggregate.distributions.Aggregate.info` is a
22
+ multi-line string. We expose the string verbatim; clients can
23
+ display it monospaced. A future structured form is a v1.1
24
+ enhancement.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import Annotated, Any, Literal
30
+
31
+ from pydantic import BaseModel, ConfigDict, Field
32
+
33
+ # ----------------------------------------------------------------------
34
+ # Shared response config
35
+ # ----------------------------------------------------------------------
36
+ # Pulled out into a constant so every response model uses identical
37
+ # settings. ``extra="forbid"`` enforces that response models only
38
+ # carry declared fields (catches accidental leakage of internal data).
39
+ _RESPONSE_CFG = ConfigDict(extra="forbid")
40
+
41
+ # A combined ratio, expected loss over premium. The library's own rule, which
42
+ # this mirrors so a bad value is a 422 from the edge rather than a ValueError
43
+ # from inside the build: "a positive finite number". No upper bound, because a
44
+ # ratio above 1 is a cover priced below its expected loss, which is a thing a
45
+ # reader may legitimately want to look at.
46
+ _Ratio = Annotated[float, Field(gt=0, allow_inf_nan=False)]
47
+
48
+
49
+ # ======================================================================
50
+ # Objects -- POST /v1/objects and friends
51
+ # ======================================================================
52
+
53
+ class BuildRequest(BaseModel):
54
+ """Body for ``POST /v1/objects``.
55
+
56
+ ``log2`` and ``bs`` are optional -- the underlying ``build()``
57
+ will choose sensible defaults when they're omitted.
58
+ """
59
+
60
+ decl: str = Field(..., min_length=1, description="DecL source text.")
61
+ log2: int | None = Field(
62
+ None,
63
+ ge=4,
64
+ description="log2 of the FFT grid size. None means 'let the library pick'.",
65
+ )
66
+ bs: float | None = Field(
67
+ None,
68
+ gt=0,
69
+ description="Bucket size. None means 'let the library pick'.",
70
+ )
71
+
72
+
73
+ class ExhibitCapability(BaseModel):
74
+ """One exhibit an object can serve, as the capability block reports it."""
75
+
76
+ model_config = _RESPONSE_CFG
77
+
78
+ name: str
79
+ title: str
80
+ # ``raw`` / ``insurer``; the two with a 1.0 implementation. An exhibit whose
81
+ # predicate fails is absent from the list rather than present with none.
82
+ perspectives: list[str] = []
83
+
84
+
85
+ class Capability(BaseModel):
86
+ """What this object can answer, computed by the library, never declared.
87
+
88
+ Rides inline on the build response rather than answering a second request,
89
+ because the navigation has to paint immediately and a round trip per build
90
+ to learn the menu is a round trip too many. ``exhibits`` is the same payload
91
+ ``GET /v1/objects/{id}/exhibits`` serves, from the same helper.
92
+
93
+ Two facts are deliberately **not** here. ``kind`` and ``has_reins`` already
94
+ ride on the build response, and one field per fact is the point of the
95
+ block: the app's two hand-written per-kind tables died so that nothing says
96
+ the same thing twice.
97
+ """
98
+
99
+ model_config = _RESPONSE_CFG
100
+
101
+ exhibits: list[ExhibitCapability] = []
102
+ charts: list[str] = []
103
+ # Which of `charts` is the object's own picture, which is what the Overview
104
+ # Plot leaf draws. `charts` answers what *can* be drawn (a reinsured
105
+ # aggregate answers two things); this answers which one to draw with no
106
+ # other instruction. None where nothing claims the object, and the app then
107
+ # says the picture does not exist yet.
108
+ primary_chart: str | None = None
109
+ # Flags for the leaves that are app behavior rather than a library
110
+ # document; each names its consumer in ``capability.py``.
111
+ has_premium: bool = False
112
+ # The premium itself, for the Evaluate form to prefill. `has_premium` is the
113
+ # yes or no the PnL form asks; this is the number, and null wherever that
114
+ # flag is false, so the two cannot disagree.
115
+ premium: float | None = None
116
+ can_sharpen: bool = False
117
+ # Whether a probe has already **run**, which is a different question from
118
+ # whether running one is worth offering: an object can answer True to both.
119
+ # Gates the More group's Sharpen leaf, since `sharpen_df` is None until then.
120
+ has_sharpen: bool = False
121
+ can_pnl: bool = False
122
+ # Can the wrap write each ceded premium as a `rate` clause? `can_pnl` plus
123
+ # the installed library accepting `pnl_program(premium_style=)`, which is an
124
+ # upstream ask; the two rate menu items grey off this until it ships.
125
+ can_pnl_rate: bool = False
126
+ # Can the Pentagon figure be drawn? A P&L, plus a library serving
127
+ # `pentagon_df`, which landed upstream at 1.0.0a393. A flag rather than an
128
+ # exhibit gate because the figure is a bespoke SVG this app draws, not a
129
+ # library document.
130
+ can_pnl_pentagon: bool = False
131
+ # Can one press produce the exploded P&L? A non-P&L answers when it can
132
+ # wrap and its engine is a single aggregate; a P&L answers through
133
+ # `can_explode`. Gates the xPnL menu item.
134
+ can_xpnl: bool = False
135
+ # Can this P&L be walked layer by layer? The explode route's own gate: a
136
+ # `pnl` over a single aggregate, so False once exploded and False over a
137
+ # portfolio engine.
138
+ can_explode: bool = False
139
+ # Can the object's realized grid be pinned into its own `hints{}`? Gates the
140
+ # action row's Hints button. True for exactly an Aggregate and a Portfolio
141
+ # today, the same pair as `can_pnl`, and kept separate because they answer
142
+ # different questions.
143
+ can_hints: bool = False
144
+ can_reins: bool = False
145
+ # Can the program be re-read as a gross / ceded / net pair? Gates the action
146
+ # row's GCN control. Narrower than `can_reins` and than `has_reins`: the
147
+ # three view prefixes build the joint *per-occurrence* aggregate, so an
148
+ # occurrence cession is required, not merely some cession.
149
+ can_views: bool = False
150
+ # Which calibration bases the reins pricing form may offer, known at build
151
+ # time so the buttons the object cannot answer grey rather than 400. Empty
152
+ # for an object with no cession.
153
+ reins_bases: list[str] = []
154
+ can_price: bool = False
155
+ can_evaluate: bool = False
156
+ can_bounds: bool = False
157
+ can_allocate: bool = False
158
+ # Are there parts to split one premium across: the units of a book, or the
159
+ # two halves of an occurrence program? Gates the Pricing group's Allocate
160
+ # leaf. A near neighbor of `can_allocate` and a different question: that one
161
+ # is the Bounds group's per-unit range and is a portfolio alone.
162
+ can_natural_allocation: bool = False
163
+ needs_premium: bool = False
164
+
165
+
166
+ class Quantile(BaseModel):
167
+ """One probability and the loss at it, exact and rounded.
168
+
169
+ Both, because they answer different questions. ``snapped`` is what the
170
+ reinsurance quick-edit form writes into a program a person then reads, and
171
+ a layer is quoted at three significant figures; ``q`` is what anyone
172
+ checking the arithmetic wants.
173
+ """
174
+
175
+ model_config = _RESPONSE_CFG
176
+
177
+ p: float
178
+ q: float
179
+ snapped: float
180
+
181
+
182
+ class QuantilesResponse(BaseModel):
183
+ """Quantiles at the requested probabilities, in the order asked."""
184
+
185
+ model_config = _RESPONSE_CFG
186
+
187
+ quantiles: list[Quantile]
188
+
189
+
190
+ class Component(BaseModel):
191
+ """One half of an object built from a pair: its grid and its moments.
192
+
193
+ A ``BivariateAggregate`` measures a grid per axis, so ``bs`` and ``log2``
194
+ are two numbers rather than one and the scalar headline fields cannot
195
+ carry them. This is what the status strip reads to print
196
+ ``bs = (a, b) · log2 = (m, n)``.
197
+
198
+ Every field is optional because a component is an ordinary ``Aggregate``
199
+ read through ``getattr``, and a kind that does not carry a moment reports
200
+ ``None`` rather than raising.
201
+ """
202
+
203
+ model_config = _RESPONSE_CFG
204
+
205
+ name: str
206
+ bs: float | None = None
207
+ log2: int | None = None
208
+ mean: float | None = None
209
+ cv: float | None = None
210
+
211
+
212
+ class BuildResponse(BaseModel):
213
+ """Slim response so the post-build page doesn't pay for unused data.
214
+
215
+ The SPA's per-button buttons (info, summary, plot, etc.) each
216
+ hit their own endpoint. With ``cached=True`` the data calls are
217
+ O(1) -- effectively the same as if the build returned everything
218
+ eagerly, minus the wasted serialization.
219
+ """
220
+
221
+ model_config = _RESPONSE_CFG
222
+
223
+ id: str
224
+ # The parser's own kind vocabulary. ``bvagg`` (not ``bivariate``) because
225
+ # where the api and the library disagree on a name, the library wins.
226
+ kind: Literal["agg", "port", "sev", "distortion", "bvagg", "pnl"]
227
+ name: str
228
+ warnings: list[str] = []
229
+ cached: bool
230
+ elapsed_ms: int
231
+ # Headline stats for the SPA's one-line build summary. Optional so a
232
+ # future object kind without these accessors still serializes. ``bs`` and
233
+ # ``log2`` are the *resolved* grid (the library's auto-pick when the
234
+ # request said "auto"), and they belong together: bs alone says how fine
235
+ # the grid is and log2 says how far it reaches, so a reader given one of
236
+ # them cannot tell whether the window covers the distribution.
237
+ bs: float | None = None
238
+ log2: int | None = None
239
+ mean: float | None = None
240
+ cv: float | None = None
241
+ validation: str | None = None
242
+ # Does this object carry a cession? Two consumers: the SPA greys out the
243
+ # Reins tab when it does not (rather than opening a pane that says "no
244
+ # reinsurance on this object"), and it decides whether Price offers the
245
+ # gross / net basis selector. Cheap: read off the cession specs, never off
246
+ # ``reins_summary_df``, which would materialize a frame on every build.
247
+ has_reins: bool = False
248
+ # The largest occurrence limit the program declares, or ``None`` where the
249
+ # cover is unlimited or the kind declares none. Rides here rather than on
250
+ # ``/meta`` for the reason the note and the tags do: the Quick Re row caps
251
+ # its tower at the policy limit, so it needs the number on every build and
252
+ # not only while one pane is open. See ``layer_pricing.policy_limit``.
253
+ policy_limit: float | None = None
254
+ # The loss / payoff sign convention, on every object that has one.
255
+ #
256
+ # a57 sent it only for ``'payoff'``, to keep the status strip's first line
257
+ # from spending a word to say "normal". The result was that it printed for
258
+ # nothing: an ``Aggregate`` and a ``Portfolio`` both answer ``'loss'`` and
259
+ # were suppressed, and a ``PnL`` carries no ``value_type`` at all, so the
260
+ # field the author asked for never once appeared. ``None`` now means the
261
+ # kind has no orientation to report, not that its orientation is ordinary.
262
+ value_type: str | None = None
263
+ # The program's ``note{}`` body, verbatim, or ``None`` when it has none (or
264
+ # the kind carries no note at all), and its ``tags{}``. The status strip
265
+ # prints both under the facts line on every build, which is why they ride on
266
+ # the build response rather than being fetched from ``/meta``: that route was
267
+ # asked for once per object and only while the Overview group was open, so
268
+ # material the reader is meant to see whatever they are looking at could not
269
+ # come from there. The Overview header that did read it is gone at a120.
270
+ note: str | None = None
271
+ tags: list[str] = []
272
+ # Per-component grid and moments, for an object built from a pair. Empty
273
+ # for every kind but ``bvagg``, whose ``bs`` is genuinely two numbers (one
274
+ # grid per axis) and whose scalar fields above are therefore all ``None``.
275
+ #
276
+ # Additive on purpose: widening ``bs`` / ``log2`` / ``mean`` / ``cv`` to
277
+ # "scalar or pair" would change the shape every consumer reads, for one
278
+ # kind's benefit. See ``routes.objects._component_fields``.
279
+ components: list[Component] = []
280
+ # What this object can answer. The SPA paints its navigation from this and
281
+ # holds no per-kind table of its own.
282
+ capability: Capability = Field(default_factory=Capability)
283
+
284
+
285
+ class ValueResponse(BaseModel):
286
+ """A program that means a number, from ``POST /v1/objects``.
287
+
288
+ DecL's top-level ``answer`` rule carries ``expr``, so ``(2+2)``, ``2/3``,
289
+ ``(2**10)`` and ``(exp(1))`` are programs like any other and ``build()``
290
+ answers each with a float. The api used to build them and then refuse the
291
+ result at the classify step, with "api supports 'agg', 'port', ... only; got
292
+ 'float'". Serving it instead is the purist reading rather than an exception
293
+ to it: the library owns what a program means, including when it means a
294
+ number.
295
+
296
+ Notes
297
+ -----
298
+ Almost none of :class:`BuildResponse` applies. There is no object, so no
299
+ ``id`` to fetch panes against, no grid, no capability block and no cache
300
+ slot; a discriminated union on ``kind`` says that in the schema rather than
301
+ leaving a caller to read six null fields and infer it.
302
+
303
+ The float travels raw. The app formats it with the same helper that prints
304
+ ``mean`` and ``cv`` off a build, which arrive equally bare, so no format is
305
+ invented here for a number the library hands over without one.
306
+ """
307
+
308
+ model_config = _RESPONSE_CFG
309
+
310
+ kind: Literal["value"] = "value"
311
+ value: float = Field(..., description="What the program evaluates to.")
312
+ decl: str = Field(..., description="The program, as the collapse left it.")
313
+ elapsed_ms: int
314
+
315
+
316
+ class LayerRequest(BaseModel):
317
+ """One layer to quote, in the vocabulary the Quick Re row speaks.
318
+
319
+ The span is given as the two amounts it resolved to rather than as the
320
+ probabilities it was typed as: resolving a percentage is the quantiles
321
+ route's job and the row has already been there, so sending the probability
322
+ again would ask the server to redo a lookup whose answer the caller is
323
+ holding. ``limit`` admits no infinity through JSON, so an unlimited cover
324
+ is spelled by omitting it.
325
+ """
326
+
327
+ tier: Literal["occurrence", "aggregate"] = Field(
328
+ "occurrence",
329
+ description="Which distribution the layer is cut out of.",
330
+ )
331
+ attach: float = Field(..., ge=0.0, allow_inf_nan=False)
332
+ limit: float | None = Field(
333
+ None, gt=0.0, allow_inf_nan=False,
334
+ description="Width of the layer. Omit for an unlimited cover.",
335
+ )
336
+ share: float = Field(1.0, gt=0.0, le=1.0, allow_inf_nan=False)
337
+
338
+
339
+ class LayerIndicationRequest(BaseModel):
340
+ """Body for ``POST /v1/objects/{id}/layers/indication``."""
341
+
342
+ layers: list[LayerRequest] = Field(
343
+ ..., min_length=1, max_length=64,
344
+ description="The layers to quote, answered in the order given.",
345
+ )
346
+ cede: float = Field(0.0, ge=0.0, lt=1.0)
347
+
348
+
349
+ class LayerIndication(BaseModel):
350
+ """The no-build quote for one layer.
351
+
352
+ Every figure is a reading of the gross object's own distributions, so the
353
+ whole response costs no build and is cheap enough for a preview line that
354
+ recomposes on every settled keystroke.
355
+
356
+ ``premium`` is the indication itself. There is no second field for it: the
357
+ full press can only revise this number upward, never replace it with
358
+ something unrelated, so one number is the whole answer.
359
+
360
+ ``cv``, ``lol``, ``rol`` and ``loss_ratio`` are optional because a capacity
361
+ layer above the severity limit has no expected loss to divide by, and an
362
+ unlimited layer has no rate-on-line base. ``count`` is the expected number
363
+ of claims reaching the layer and is occurrence only: on the aggregate tier
364
+ the layer is pierced by the year's total, so a claim count to it means
365
+ nothing.
366
+ """
367
+
368
+ model_config = _RESPONSE_CFG
369
+
370
+ tier: str
371
+ attach: float
372
+ # Null on an unlimited cover, the same way the request spells one.
373
+ limit: float | None = None
374
+ share: float
375
+ el: float
376
+ sd: float
377
+ cv: float | None = None
378
+ count: float | None = None
379
+ pr_attach: float
380
+ lol: float | None = None
381
+ floored: bool
382
+ premium: float
383
+ rol: float | None = None
384
+ spelling: str
385
+ cede: float
386
+ loss_ratio: float | None = None
387
+
388
+
389
+ class LayerIndicationResponse(BaseModel):
390
+ """One indication per layer, in the order asked."""
391
+
392
+ model_config = _RESPONSE_CFG
393
+
394
+ indications: list[LayerIndication]
395
+
396
+
397
+ class LayerQuote(LayerIndication):
398
+ """One layer's full quote, with the four candidates behind it.
399
+
400
+ The components ride along rather than only the winner, because which
401
+ measure bound is the interesting part: the dual transform loads the body of
402
+ a distribution and binds low in a tower, the proportional hazard transform
403
+ loads the tail and binds high. ``binds`` is ``'sd'``, ``'ph'``, ``'dual'``
404
+ or ``'min_rol'``.
405
+
406
+ ``min_rol`` is the minimum-rate-on-line premium, which is the fourth
407
+ candidate rather than a floor applied after the comparison, so ``binds``
408
+ names it where it set the price. Null on an unlimited layer, which has no
409
+ rate-on-line base and competes on three.
410
+
411
+ ``label`` is the layer's ``as "..."`` name where the clause carried one, and
412
+ otherwise ``Occ n`` / ``Agg n``, numbered within its own tier in clause
413
+ order. It is the quote sheet's first column, so a row says which tier the
414
+ layer sits on and where in the stack it is, which the span alone does not.
415
+ The fallback is the sheet's and not the program's: the clause keeps the
416
+ spec's own naming.
417
+ """
418
+
419
+ sd_load: float
420
+ ph: float
421
+ dual: float
422
+ min_rol: float | None = None
423
+ binds: str
424
+ label: str = ""
425
+
426
+
427
+ class DerivedResponse(BuildResponse):
428
+ """A derivation's result: the program that made it, and the object.
429
+
430
+ The build manifest plus the DecL text that produced it, because every
431
+ derivation in this app is a program you can see. The text lands in the
432
+ editor, so you read what was built, you can edit it, and history, sharing
433
+ and rebuild all keep working. No hidden state, and no object mutated behind
434
+ a cached id.
435
+
436
+ ``id`` is the id an ordinary build of ``program`` would produce, so
437
+ rebuilding the text from the editor is a cache hit rather than a second
438
+ build.
439
+ """
440
+
441
+ model_config = _RESPONSE_CFG
442
+
443
+ program: str = Field(..., description="The DecL that builds this object.")
444
+ description: str | None = Field(
445
+ None,
446
+ description=(
447
+ "What the derivation did, when it has something to say. Sharpen "
448
+ "fills it with the probe's verdict; the others leave it empty."
449
+ ),
450
+ )
451
+ quotes: list[LayerQuote] | None = Field(
452
+ None,
453
+ description=(
454
+ "The per-layer quote behind a priced cession, in the order the "
455
+ "clause spells it. Only a cession asked to price carries this; "
456
+ "every other derivation leaves it empty."
457
+ ),
458
+ )
459
+ ir: dict[str, Any] | None = Field(
460
+ None,
461
+ description=(
462
+ "Table document for the quote sheet, keyed 'quotes'. Carried the "
463
+ "way BoundsResponse carries its own, so the SPA can render the "
464
+ "rows in whichever table view the page is set to."
465
+ ),
466
+ )
467
+
468
+
469
+ class PnlProgramRequest(BaseModel):
470
+ """Body for ``POST /v1/objects/{id}/pnl``.
471
+
472
+ Every field here is a convention rather than a fact, which is why the
473
+ library puts them in the signature where a caller reads them, and why they
474
+ are request fields rather than server settings. The button posts an empty
475
+ body and takes all five defaults, so this model is where the app's opinion
476
+ about a demo book is written down, documented and tested.
477
+
478
+ Notes
479
+ -----
480
+ **Three of the five defaults are the app's own**, and diverge from the
481
+ library's deliberately. Upstream, all three combined ratios default to
482
+ ``None``, which means "behave exactly as before" and leaves any cession
483
+ unpriced. This endpoint exists to serve one app whose PnL button is demo
484
+ sugar, so it defaults to a priced book: a 90 percent net combined ratio,
485
+ occurrence cover at 75 and aggregate cover at 65. ``loss_ratio`` and
486
+ ``expense_ratio`` match the library's own defaults exactly, and a test
487
+ holds all five field names to the library's signature so the two cannot
488
+ drift apart unnoticed.
489
+
490
+ **The ladder makes ``loss_ratio`` legacy.** With a combined ratio in hand
491
+ the premium is built from the bottom up, net technical premium plus the
492
+ cost of each cover, grossed up once for expenses, so ``loss_ratio`` is used
493
+ only when the ladder is off, which for this endpoint means a portfolio
494
+ engine. See ``aggregate`` 1.0.0a306, ``dev/done/plan-pnl-reinsurance-pricing.md``.
495
+
496
+ **Ceded premiums are written as ``deposit`` amounts by default.**
497
+ ``premium_style='rate'`` asks the library to spell each priced layer's
498
+ premium as a ``rate`` of the P&L's stated gross premium instead. That is
499
+ not the circularity the ladder refuses on its *inputs* (a layer arriving
500
+ with a ``rate`` clause quotes a fraction of a premium the ladder has not
501
+ computed yet); here the ladder has finished and the rate is a respelling
502
+ of the resolved deposit. The keyword is an upstream ask, so the route
503
+ refuses ``'rate'`` with a 400 until the installed library's signature
504
+ accepts it; see ``capability.PNL_PREMIUM_STYLE_SUPPORTED``.
505
+
506
+ **``form`` and ``premium_style`` are the app's routing, not library
507
+ kwargs** (``premium_style`` becomes one when it ships). ``form='xpnl'``
508
+ runs the wrapped program through the same explode machinery the explode
509
+ route uses, so one press answers "the exploded P&L of what is in the box".
510
+ """
511
+
512
+ form: Literal["pnl", "xpnl"] = Field(
513
+ "pnl",
514
+ description=("Which keyword leads the derived program: 'pnl' for the "
515
+ "consolidated wrap, 'xpnl' to wrap and break the book out "
516
+ "layer by layer. 'xpnl' refuses a portfolio engine."))
517
+ premium_style: Literal["deposit", "rate"] = Field(
518
+ "deposit",
519
+ description=("How each priced layer's premium is written: a 'deposit' "
520
+ "amount, or a 'rate' of the P&L's stated gross premium. "
521
+ "'rate' needs a library whose pnl_program accepts "
522
+ "premium_style and is refused with a 400 otherwise."))
523
+
524
+ loss_ratio: float = Field(
525
+ 0.70, gt=0, le=1,
526
+ description=("Sizes the premium as expected loss over this, when there is none to "
527
+ "derive from. Unused once the combined-ratio ladder is engaged."))
528
+ expense_ratio: float = Field(
529
+ 0.25, ge=0, lt=1,
530
+ description="Gross expense as a fraction of premium; 0 omits the clause.")
531
+ net_combined_ratio: _Ratio | None = Field(
532
+ 0.90,
533
+ description=("Expected net loss over net technical premium. Engages the ladder, "
534
+ "which prices every cession; null leaves cessions unpriced."))
535
+ occ_combined_ratio: _Ratio | list[_Ratio] | None = Field(
536
+ 0.75,
537
+ description=("The occurrence tier's combined ratio: one value, or one per layer in "
538
+ "declaration order. Null means the net ratio."))
539
+ agg_combined_ratio: _Ratio | list[_Ratio] | None = Field(
540
+ 0.65,
541
+ description=("The aggregate tier's combined ratio, one value or one per layer. "
542
+ "Null means the net ratio."))
543
+
544
+
545
+ class NarrativeSection(BaseModel):
546
+ """One heading in the Narrative pane: a short form and a long one."""
547
+
548
+ model_config = _RESPONSE_CFG
549
+
550
+ name: str
551
+ description: str = ""
552
+ explanation: str = ""
553
+
554
+
555
+ class NarrativeResponse(BaseModel):
556
+ """``GET /v1/objects/{id}/narrative``: everything the object says in prose.
557
+
558
+ The ``info`` block first, then a section per text field the object carries.
559
+ Sections are found by suffix rather than listed, so a narrative the library
560
+ adds upstream appears here on its own.
561
+ """
562
+
563
+ model_config = _RESPONSE_CFG
564
+
565
+ info: str = ""
566
+ sections: list[NarrativeSection] = []
567
+
568
+
569
+ class BoundsRequest(BaseModel):
570
+ """Body for the two tabular bounds routes.
571
+
572
+ ``premium`` is the calibration: the price some distortion puts on this
573
+ object. Everything reported is the range over the distortions consistent
574
+ with it, so without a premium there is no question to ask.
575
+ """
576
+
577
+ premium: float = Field(..., gt=0, description="Target premium for this object.")
578
+ assets: float | None = Field(
579
+ None, gt=0,
580
+ description="Asset cap; prices min(X, a). Unbounded when omitted.")
581
+ against: list[str] = Field(
582
+ default_factory=list,
583
+ description=("PricingBounds only: the risks to price against this one. "
584
+ "Each is a unit of the current portfolio, or a DecL "
585
+ "fragment for a line that does not exist yet. Empty on a "
586
+ "portfolio means every unit, which is the question a "
587
+ "portfolio invites; an aggregate has no units, so empty "
588
+ "there is an error."),
589
+ )
590
+
591
+
592
+ class BoundsResponse(BaseModel):
593
+ """A bounds table: one row per unit or per named risk.
594
+
595
+ ``lower`` and ``upper`` are the ends of the consistent range and ``width``
596
+ is the reading, being how much of the price is decided by the choice of
597
+ distortion rather than by the premium the object was calibrated to.
598
+ """
599
+
600
+ model_config = _RESPONSE_CFG
601
+
602
+ premium: float
603
+ table: FrameResponse
604
+ ir: dict[str, Any] | None = Field(
605
+ None,
606
+ description="Table document for the static view, keyed 'table'.",
607
+ )
608
+
609
+
610
+ class ReinsProgramRequest(BaseModel):
611
+ """Body for ``POST /v1/objects/{id}/reins``.
612
+
613
+ ``cession`` is one clause per tier, each opening with ``occurrence`` or
614
+ ``aggregate``. A clause is authoritative for its own tier and leaves the
615
+ other alone, which is how the grammar reads it too, so composing an
616
+ occurrence and an aggregate cession means sending both rather than sending
617
+ one and then ceding again.
618
+ """
619
+
620
+ cession: str | list[str] = Field(
621
+ ...,
622
+ description=("A cession clause, or one per tier: "
623
+ "'occurrence net of 500 xs 500'."),
624
+ )
625
+ price: bool = Field(
626
+ False,
627
+ description=("Quote every layer and write the premium into the clause, "
628
+ "as a 'deposit' or a 'rol'. Costs one small auxiliary "
629
+ "build per layer."),
630
+ )
631
+ cede: float = Field(
632
+ 0.0, ge=0.0, lt=1.0,
633
+ description=("Ceding commission. The quote is the reinsurer's net, so "
634
+ "the written premium is grossed up by 1 / (1 - cede)."),
635
+ )
636
+
637
+
638
+ class ObjectSummary(BaseModel):
639
+ """One row in ``GET /v1/objects`` (cache listing)."""
640
+
641
+ model_config = _RESPONSE_CFG
642
+
643
+ id: str
644
+ kind: str
645
+ name: str
646
+ ts: str # ISO 8601
647
+
648
+
649
+ class ObjectListResponse(BaseModel):
650
+ """Wrapper for ``GET /v1/objects``."""
651
+
652
+ model_config = _RESPONSE_CFG
653
+
654
+ objects: list[ObjectSummary]
655
+
656
+
657
+ class ObjectManifest(BaseModel):
658
+ """``GET /v1/objects/{id}`` -- metadata about a single cached object."""
659
+
660
+ model_config = _RESPONSE_CFG
661
+
662
+ id: str
663
+ kind: str
664
+ name: str
665
+ decl: str
666
+ log2: int
667
+ bs: float
668
+ created_at: str # ISO 8601
669
+
670
+
671
+ class DeleteResponse(BaseModel):
672
+ model_config = _RESPONSE_CFG
673
+
674
+ ok: bool
675
+
676
+
677
+ # ======================================================================
678
+ # Tabular endpoints -- summary / stats_df / density_df / kappa
679
+ # ======================================================================
680
+
681
+ class FrameResponse(BaseModel):
682
+ """Pandas DataFrame as ``(columns, rows)``.
683
+
684
+ Rows are list-of-lists rather than list-of-dicts so a wide
685
+ density_df (50+ columns, 2**16+ rows) doesn't redundantly carry
686
+ the column name string with every cell. Trim payload by an
687
+ order of magnitude vs the dict-per-row form.
688
+ """
689
+
690
+ model_config = _RESPONSE_CFG
691
+
692
+ columns: list[str]
693
+ rows: list[list[Any]]
694
+
695
+
696
+ # ======================================================================
697
+ # Info -- raw multi-line string from Aggregate.info / Portfolio.info
698
+ # ======================================================================
699
+
700
+ class InfoResponse(BaseModel):
701
+ model_config = _RESPONSE_CFG
702
+
703
+ info: str
704
+
705
+
706
+ class ObjectMetaResponse(BaseModel):
707
+ """``GET /v1/objects/{id}/meta`` -- the object's own DecL metadata.
708
+
709
+ The trailer clauses a first-class citizen carries (``note`` / ``tags`` /
710
+ ``hints``) plus the two program renderings. Those three are the whole
711
+ trailer since ``aggregate`` 1.0.0a301.
712
+
713
+ ``program`` is what the parser was handed, after preprocessing (folded onto
714
+ one line, comments stripped), not the user's keystrokes. ``pprogram`` is
715
+ what the parser understood, re-rendered canonically, and is the one to show
716
+ a reader.
717
+
718
+ Every field is optional. They are read through ``getattr`` so a class that
719
+ does not carry one reports ``None`` rather than raising.
720
+ """
721
+
722
+ model_config = _RESPONSE_CFG
723
+
724
+ kind: str
725
+ name: str
726
+ note: str | None = None
727
+ tags: list[str] = []
728
+ hints: str | None = None
729
+ program: str | None = None
730
+ pprogram: str | None = None
731
+
732
+
733
+ # ======================================================================
734
+ # Reinsurance -- text description block
735
+ # ======================================================================
736
+
737
+ class ReinsDescriptionResponse(BaseModel):
738
+ """``GET /v1/objects/{id}/reins_description``.
739
+
740
+ ``available`` is False (and ``text`` empty) when the object carries
741
+ no reinsurance, so the SPA can show a neutral "no reinsurance on this
742
+ object" line rather than an error.
743
+ """
744
+
745
+ model_config = _RESPONSE_CFG
746
+
747
+ available: bool
748
+ text: str
749
+
750
+
751
+ # ======================================================================
752
+ # Pricing
753
+ # ======================================================================
754
+
755
+ class PricingPreviewRequest(BaseModel):
756
+ """Body for ``POST /v1/objects/{id}/pricing/preview``.
757
+
758
+ The same anchor and target the calibration takes, because the preview's job
759
+ is to say what that calibration is about to be struck at. ``basis`` names the
760
+ reinsurance view for a reinsured Aggregate and is rejected elsewhere.
761
+ """
762
+
763
+ p: float | None = Field(
764
+ None, gt=0, le=1, description="VaR probability in (0, 1] fixing capital.")
765
+ a: float | None = Field(
766
+ None, gt=0, description="Asset level fixing capital; snapped to the grid.")
767
+ coc: float | None = Field(None, gt=0, description="Cost-of-capital (ROE) target.")
768
+ lr: float | None = Field(None, gt=0, description="Loss-ratio target.")
769
+ premium: float | None = Field(
770
+ None, gt=0, description="Premium target; the pentagon's ``P``.")
771
+ basis: str | None = Field(
772
+ None, description="Reinsurance view: 'gross', 'net occ' or 'net'.")
773
+ expense_ratio: float | None = Field(
774
+ None, ge=0, lt=1,
775
+ description=("Gross expense as a fraction of premium. A premium "
776
+ "target is read as gross and the pentagon runs on "
777
+ "premium × (1 − e); the response reports gross_premium. "
778
+ "Refused for a P&L, whose ledger states its own "
779
+ "expenses."))
780
+
781
+
782
+ class PricingPreviewResponse(BaseModel):
783
+ """The completed pentagon as scalars, for the Calibrate form's preview line.
784
+
785
+ The octet under wire names: ``loss``, ``margin``, ``premium``, ``capital``
786
+ and ``assets`` are the five levels (``L``, ``M``, ``P``, ``Q``, ``a``), and
787
+ ``lr``, ``pq``, ``coc`` the three ratios between them. ``p`` echoes the
788
+ probability the caller named, and is null when they anchored on assets.
789
+
790
+ ``premium`` is always the technical premium, net of expenses. The three
791
+ fields after the octet are the gross story. ``gross_premium`` rides when the
792
+ request carried an ``expense_ratio`` (the resolved technical premium grossed
793
+ back up) and on a P&L, where it is the ledger's own stated gross.
794
+ ``net_premium`` and ``net_of_expense_premium`` are a P&L's only, ``P`` and
795
+ ``P − E`` off the ledger's **closing** block: the position once every tier's
796
+ program has worked, which is what a reader looking at a peeled P&L is
797
+ pricing. Both are null everywhere else. On a single-group ledger the closing
798
+ block is the gross block and all three agree.
799
+ """
800
+
801
+ model_config = _RESPONSE_CFG
802
+
803
+ p: float | None = None
804
+ assets: float | None = None
805
+ loss: float | None = None
806
+ margin: float | None = None
807
+ premium: float | None = None
808
+ capital: float | None = None
809
+ lr: float | None = None
810
+ pq: float | None = None
811
+ coc: float | None = None
812
+ gross_premium: float | None = None
813
+ net_premium: float | None = None
814
+ net_of_expense_premium: float | None = None
815
+
816
+
817
+ class PnLPentagonRequest(BaseModel):
818
+ """Body for ``POST /v1/objects/{id}/pnl/pentagon``.
819
+
820
+ One field, and it is a **list**, which is the whole shape of the request.
821
+ The Pentagon leaf's control is a strip of mini pentagons, one per solvency
822
+ level, each labeled with the assets that level implies, and the reader picks
823
+ from what they can already see. So the answer has to carry every level at
824
+ once: serving one and re-asking on each press would mean the strip could
825
+ not label itself until the reader had pressed all six.
826
+ """
827
+
828
+ periods: list[float] | None = Field(
829
+ None, min_length=1, max_length=12,
830
+ description=("Return periods to answer at, so the 1-in-t state fixes "
831
+ "capital on each. Defaults to the strip's own six. Each "
832
+ "must exceed 1."))
833
+
834
+
835
+ class PnLPentagonLevel(BaseModel):
836
+ """One solvency level's pentagon: the level, and the frame struck at it."""
837
+
838
+ model_config = _RESPONSE_CFG
839
+
840
+ t: float
841
+ frame: FrameResponse
842
+
843
+
844
+ class PnLPentagonResponse(BaseModel):
845
+ """``POST /v1/objects/{id}/pnl/pentagon`` -- the ledger as a pentagon.
846
+
847
+ One entry per requested level, in the order asked for, so the first is the
848
+ strip's default. Each frame is ``PnL.pentagon_df`` with its index reset, so
849
+ ``Step`` is the first column: the figure picks ``Gross``, ``Ceded`` and
850
+ ``All`` and ignores the rest, which are the ledger's own steps and are what
851
+ a future stepper would walk.
852
+
853
+ The amounts are in the object's own currency units and the ratios are
854
+ fractions. Nothing here is formatted: the browser owns presentation, as it
855
+ does for every table in the app.
856
+ """
857
+
858
+ model_config = _RESPONSE_CFG
859
+
860
+ levels: list[PnLPentagonLevel]
861
+
862
+
863
+ class PricingCalibrateRequest(BaseModel):
864
+ """Body for ``POST /v1/objects/{id}/pricing/calibrate``.
865
+
866
+ Exactly one capital anchor (``p`` or ``a``) and exactly one pricing target
867
+ (``coc``, ``lr`` or ``premium``). The library owns the loss-ratio
868
+ conversion, so ``lr`` travels as itself rather than being turned into a cost
869
+ of capital here.
870
+
871
+ ``premium`` is the third target since 1.0.0a100, because
872
+ :meth:`price_pentagon` has always taken one and the Bounds forms ask the
873
+ same question in that spelling. It costs one extra library call: see
874
+ :func:`aggregate_api.pricing.run_calibration`.
875
+ """
876
+
877
+ p: float | None = Field(
878
+ None, gt=0, le=1, description="VaR probability in (0, 1] fixing capital.")
879
+ a: float | None = Field(
880
+ None, gt=0, description="Asset level fixing capital; snapped to the grid.")
881
+ coc: float | None = Field(None, gt=0, description="Cost-of-capital (ROE) target.")
882
+ lr: float | None = Field(None, gt=0, description="Loss-ratio target.")
883
+ premium: float | None = Field(
884
+ None, gt=0, description="Premium target; the pentagon's ``P``.")
885
+ basis: str | None = Field(
886
+ None, description="Calibration basis: 'gross', 'net occ' or 'net'.")
887
+ expense_ratio: float | None = Field(
888
+ None, ge=0, lt=1,
889
+ description=("Gross expense as a fraction of premium. A premium "
890
+ "target is read as gross and the fit runs on "
891
+ "premium × (1 − e); a CoC or LR target is unaffected."))
892
+
893
+
894
+ class PricingAllocateRequest(PricingCalibrateRequest):
895
+ """Body for ``POST /v1/objects/{id}/pricing/allocate``.
896
+
897
+ The calibrate shape exactly, because the allocation is a calibration that is
898
+ then decomposed: the same anchor and the same target, struck once and split
899
+ across the parts. It is a separate model rather than a reuse so the basis
900
+ field can carry the narrower rule this route enforces.
901
+
902
+ ``basis`` is gated. The natural allocation splits a **gross** premium across
903
+ an occurrence program, so an ``Aggregate`` takes ``gross`` or nothing;
904
+ anything else is an HTTP 400. A ``Portfolio`` takes ``net`` or nothing, the
905
+ one basis a book answers end to end, matching the row the Calibrate form
906
+ offers it.
907
+ """
908
+
909
+ basis: str | None = Field(
910
+ None,
911
+ description=("Calibration basis: 'gross' for an Aggregate, 'net' for a "
912
+ "Portfolio. Omit to take the object's own."),
913
+ )
914
+
915
+
916
+ class PricingEvaluateRequest(BaseModel):
917
+ """Body for ``POST /v1/objects/{id}/pricing/evaluate``.
918
+
919
+ Every field is optional and every field is refused for a P&L, which carries
920
+ its premium in its ledger and evaluates each row on that row's own terms.
921
+ ``basis`` names **which premium is being input** on a reinsured Aggregate,
922
+ which is a narrower question than the gross versus net comparison the
923
+ Economics group answers.
924
+ """
925
+
926
+ premium: float | None = Field(
927
+ None,
928
+ gt=0,
929
+ description=(
930
+ "The consideration held against this position. Omit to use the "
931
+ "object's own; rejected for a P&L, whose ledger carries it."
932
+ ),
933
+ )
934
+ basis: str | None = Field(
935
+ None,
936
+ description=("Which premium this is: 'gross', 'net occ' or 'net'. "
937
+ "A reinsured Aggregate only."),
938
+ )
939
+ p: float | None = Field(
940
+ None, gt=0, le=1,
941
+ description="VaR probability fixing the asset level the panel is solved at.")
942
+ a: float | None = Field(
943
+ None, gt=0, description="Asset level the panel is solved at.")
944
+ expense_ratio: float | None = Field(
945
+ None, ge=0, lt=1,
946
+ description=("Gross expense as a fraction of premium. A typed "
947
+ "premium is read as gross and evaluated at "
948
+ "premium × (1 − e). Refused for a P&L, whose ledger "
949
+ "states its own expenses."))
950
+
951
+
952
+ class PricingExhibitsResponse(BaseModel):
953
+ """One or more library exhibits, each under both perspectives.
954
+
955
+ The shape all three of ``pricing/calibrate``, ``pricing/allocate`` and
956
+ ``pricing/evaluate`` answer with. ``exhibits`` maps the registry name
957
+ (``pricing.calibrate``, ``pricing.stand_alone``, ``pricing.allocate``,
958
+ ``pricing.evaluate``) to a map of perspective to envelope, the same envelope
959
+ ``GET /objects/{id}/exhibit/{name}`` serves.
960
+
961
+ Bundling both perspectives is deliberate. The frames are small, the pane's
962
+ RAW / INSURER toggle then flips with no recompute, and the alternative would
963
+ be caching a result object server side so a second request could answer the
964
+ other reading of a calibration that has already been made.
965
+
966
+ ``warnings`` carries what the library said on the way, verbatim. A distortion
967
+ it declines to allocate is the standing case: the table shows the families
968
+ that answered and this says which one did not, and why.
969
+ """
970
+
971
+ model_config = _RESPONSE_CFG
972
+
973
+ kind: str
974
+ exhibits: dict[str, dict[str, Any]]
975
+ warnings: list[str] = []
976
+
977
+
978
+ class RuinRequest(BaseModel):
979
+ """Body for ``POST /v1/objects/{id}/ruin``.
980
+
981
+ The premium half is the Calibrate shape exactly: one capital anchor
982
+ (``p`` or ``a``) and one pricing target (``coc``, ``lr`` or ``premium``),
983
+ from which the runner derives the loss ratio the ruin engine takes,
984
+ through :meth:`price_pentagon` where the target is not already a loss
985
+ ratio. There is no ``basis``: the ruin reading is of the object's own
986
+ law, and a margin struck on another view would price a different
987
+ distribution than the one being simulated.
988
+
989
+ ``ruin_p`` is the initial capital, entered as a probability of eventual
990
+ default and resolved to a surplus through the ruin function's capital
991
+ lookup; ``u`` states the surplus directly. At most one travels; with
992
+ neither, the library's teaching default applies. The name is not ``p``
993
+ because ``p`` is already the VaR anchor everywhere on this surface.
994
+
995
+ ``sample`` is the Sample action: the server draws one fresh integer
996
+ seed, uses it for both the chart and the exhibit so they describe the
997
+ same draw, and reports it in the chart document's ``meta``. It is a
998
+ flag rather than ``seed: null`` because a typed model cannot tell an
999
+ omitted field from an explicit null on the wire.
1000
+ """
1001
+
1002
+ p: float | None = Field(
1003
+ None, gt=0, le=1, description="VaR probability in (0, 1] fixing capital.")
1004
+ a: float | None = Field(
1005
+ None, gt=0, description="Asset level fixing capital; snapped to the grid.")
1006
+ coc: float | None = Field(None, gt=0, description="Cost-of-capital (ROE) target.")
1007
+ lr: float | None = Field(None, gt=0, description="Loss-ratio target.")
1008
+ premium: float | None = Field(
1009
+ None, gt=0, description="Premium target; the pentagon's ``P``.")
1010
+ ruin_p: float | None = Field(
1011
+ None, gt=0, lt=1,
1012
+ description=("Probability of eventual default; resolved to an initial "
1013
+ "surplus through the ruin function's capital lookup."))
1014
+ u: float | None = Field(
1015
+ None, ge=0, description="Initial surplus directly. At most one of "
1016
+ "``ruin_p`` or ``u``.")
1017
+ seed: int | None = Field(
1018
+ None, ge=0,
1019
+ description=("rng seed for the simulated paths. Omit for the "
1020
+ "library's fixed teaching seed; ignored when ``sample`` "
1021
+ "is set."))
1022
+ sample: bool = Field(
1023
+ False,
1024
+ description=("Draw a fresh seed server side and report it in the "
1025
+ "chart document's ``meta.seed``."))
1026
+ n_plot: int | None = Field(
1027
+ None, ge=1, le=200, description="Sample paths drawn; the library "
1028
+ "default is 50.")
1029
+ detail: int | None = Field(
1030
+ None, ge=16, description="Per-path point budget for the display "
1031
+ "decimation.")
1032
+
1033
+
1034
+ class RuinResponse(BaseModel):
1035
+ """Answer for ``POST /v1/objects/{id}/ruin``.
1036
+
1037
+ ``chart`` is the ``ruin`` chart document, byte for byte what the generic
1038
+ chart route would serve, parsed so it rides inside a JSON response; its
1039
+ ``meta`` carries the scalars the pane labels itself with (the resolved
1040
+ ``u``, exact and simulated psi, the seed). ``exhibits`` maps ``ruin`` to
1041
+ a perspective-to-envelope pair, the :class:`PricingExhibitsResponse`
1042
+ shape, because the exhibit registers on the
1043
+ :class:`~aggregate.results.RuinResult` the request builds rather than on
1044
+ the cached object.
1045
+ """
1046
+
1047
+ model_config = _RESPONSE_CFG
1048
+
1049
+ kind: str
1050
+ chart: dict[str, Any]
1051
+ exhibits: dict[str, dict[str, Any]]
1052
+ warnings: list[str] = []
1053
+
1054
+
1055
+ # ======================================================================
1056
+ # DecL helpers
1057
+ # ======================================================================
1058
+
1059
+ class DeclCompleteRequest(BaseModel):
1060
+ decl: str
1061
+ cursor: int = Field(..., ge=0)
1062
+
1063
+
1064
+ class Completion(BaseModel):
1065
+ model_config = _RESPONSE_CFG
1066
+
1067
+ #: What to **insert**: the bare token, always valid DecL on its own.
1068
+ #:
1069
+ #: Added at a67. Editors were inserting ``label``, which for the 38 of 105
1070
+ #: terminals carrying a gloss is a whole phrase and not a token, so
1071
+ #: accepting a completion could put ``after' (profit-commission allowance)``
1072
+ #: into a program. Nothing consuming this should insert anything else.
1073
+ text: str
1074
+ label: str
1075
+ #: The gloss, where the terminal has one: ``or 'approx'``. Display only.
1076
+ detail: str | None = None
1077
+ terminal: str
1078
+ kind: Literal["keyword", "identifier", "literal"]
1079
+
1080
+
1081
+ class CompletionsResponse(BaseModel):
1082
+ model_config = _RESPONSE_CFG
1083
+
1084
+ completions: list[Completion]
1085
+
1086
+
1087
+ class DeclLexRequest(BaseModel):
1088
+ decl: str
1089
+
1090
+
1091
+ class DeclFormatRequest(BaseModel):
1092
+ decl: str
1093
+
1094
+
1095
+ class DeclFormatResponse(BaseModel):
1096
+ """``POST /v1/decl/format`` -- canonicalized DecL.
1097
+
1098
+ ``decl`` is the program re-rendered through ``aggregate``'s
1099
+ ``format_program`` (canonical clause order / spacing). On a format
1100
+ failure the original text is echoed back unchanged.
1101
+ """
1102
+
1103
+ model_config = _RESPONSE_CFG
1104
+
1105
+ decl: str
1106
+
1107
+
1108
+ class DeclParseRequest(BaseModel):
1109
+ decl: str
1110
+
1111
+
1112
+ class ParsedStatement(BaseModel):
1113
+ """One statement of a program, as the parser understood it.
1114
+
1115
+ ``kind`` and ``name`` are the first two members of the
1116
+ ``(kind, name, spec)`` triple ``aggregate``'s parser returns: the object
1117
+ kind the statement declares (``agg``, ``sev``, ``port``, ``pnl``, ``xpnl``,
1118
+ ``distortion``, and so on) and the name it declares it under.
1119
+
1120
+ Notes
1121
+ -----
1122
+ ``spec`` is deliberately an open ``dict``, not a mirrored schema. Its keys
1123
+ are the **library's** vocabulary and they move when the grammar moves, so a
1124
+ typed model here would be a second declaration of the transformer's output:
1125
+ it would drift, and then it would start rejecting specs the library had
1126
+ legitimately grown. The route's whole promise is "what the parser
1127
+ understood, coerced to JSON".
1128
+
1129
+ Two coercions a reader has to know about. A tuple arrives as an array, so a
1130
+ reinsurance layer reads as the three-element ``[share, limit, attach]`` and
1131
+ not as an object. And a non-finite float arrives as the string ``"inf"``,
1132
+ ``"-inf"`` or ``"nan"``, which is both the spelling the grammar accepts and
1133
+ the only form that keeps an unlimited limit distinguishable from an absent
1134
+ one. See :func:`aggregate_api.serializers.spec_to_payload`.
1135
+ """
1136
+
1137
+ model_config = _RESPONSE_CFG
1138
+
1139
+ kind: str
1140
+ name: str
1141
+ spec: dict[str, Any]
1142
+
1143
+
1144
+ class DeclParseResponse(BaseModel):
1145
+ """``POST /v1/decl/parse`` -- the parser's answer as data.
1146
+
1147
+ One entry per statement, in source order. Nothing is built, cached or
1148
+ audited on the way.
1149
+ """
1150
+
1151
+ model_config = _RESPONSE_CFG
1152
+
1153
+ statements: list[ParsedStatement]
1154
+
1155
+
1156
+ class LexToken(BaseModel):
1157
+ model_config = _RESPONSE_CFG
1158
+
1159
+ type: str
1160
+ value: str
1161
+ start: int
1162
+ end: int
1163
+ line: int
1164
+ column: int
1165
+
1166
+
1167
+ class LexResponse(BaseModel):
1168
+ model_config = _RESPONSE_CFG
1169
+
1170
+ tokens: list[LexToken]
1171
+
1172
+
1173
+ # ======================================================================
1174
+ # Examples (aggregate's library.agg, via the recipe base)
1175
+ # ======================================================================
1176
+
1177
+ class Pill(BaseModel):
1178
+ """One render-ready label on an example row.
1179
+
1180
+ ``ns`` is the namespace the value came from and is what the SPA colors on:
1181
+ ``kind`` off the recipe index, ``topic`` and ``role`` off the tag slugs. The
1182
+ value is the bare word, so ``topic:reinsurance`` draws as ``reinsurance``,
1183
+ and it is the text inside the pill, which is why a reader who cannot
1184
+ separate the three hues still reads what the pill says.
1185
+ """
1186
+
1187
+ model_config = _RESPONSE_CFG
1188
+
1189
+ ns: Literal["kind", "topic", "role"]
1190
+ value: str
1191
+
1192
+
1193
+ class FacetValue(BaseModel):
1194
+ """One value of one facet, with how many of the returned entries carry it."""
1195
+
1196
+ model_config = _RESPONSE_CFG
1197
+
1198
+ value: str
1199
+ count: int
1200
+
1201
+
1202
+ class ExampleItem(BaseModel):
1203
+ """One library entry.
1204
+
1205
+ ``note`` is preferred but never guaranteed: most entries carry one, and an
1206
+ entry without a note is ordinary, not defective. ``decl`` is the entry's
1207
+ DecL as its ``.agg`` file spells it (``Recipe.as_read``), several lines,
1208
+ indented as written and carrying its trailer, so it drops straight into the
1209
+ editor teaching the spelling the author chose.
1210
+
1211
+ ``pills`` is the row as it should be drawn, already ordered kind, then
1212
+ topics, then roles. ``tags`` is the same information as full slugs, kept for
1213
+ the search haystack and for anyone reading the api directly, and ``kind``
1214
+ keeps a field of its own because the recipe's type is not a tag.
1215
+ """
1216
+
1217
+ model_config = _RESPONSE_CFG
1218
+
1219
+ name: str
1220
+ kind: str
1221
+ tags: list[str] = []
1222
+ note: str | None = None
1223
+ decl: str
1224
+ pills: list[Pill] = []
1225
+
1226
+
1227
+ class ExamplesResponse(BaseModel):
1228
+ """``GET /v1/examples`` -- the whole library, one flat list in file order.
1229
+
1230
+ ``items`` is the library in its own reading order (``Recipe.seq``), each
1231
+ entry appearing exactly once. There are no groups and no headings: the
1232
+ ``# ---`` banners in ``library.agg`` are comments, nothing in ``aggregate``
1233
+ parses them, and reading order is what the file actually carries.
1234
+
1235
+ ``facets`` is keyed by the three pill namespaces, and each list is ordered by
1236
+ first appearance in ``items``, so a filter bar built from it reads in file
1237
+ order too. Counts are over the entries returned, so a filtered payload adds
1238
+ up to what the caller can see.
1239
+ """
1240
+
1241
+ model_config = _RESPONSE_CFG
1242
+
1243
+ items: list[ExampleItem]
1244
+ facets: dict[str, list[FacetValue]]
1245
+
1246
+
1247
+ class HeroesResponse(BaseModel):
1248
+ """``GET /v1/examples/heroes`` -- the entries tagged ``role:hero``."""
1249
+
1250
+ model_config = _RESPONSE_CFG
1251
+
1252
+ items: list[ExampleItem]
1253
+
1254
+
1255
+ class SparklinesResponse(BaseModel):
1256
+ """``GET /v1/examples/heroes/sparklines`` -- thumbnail silhouettes.
1257
+
1258
+ ``sparklines`` maps an entry name to a short list of values in ``[0, 1]``,
1259
+ the density binned and scaled so its peak is 1. Shape only: there are no
1260
+ axes, no units and no way to read a number off it, which is what a card
1261
+ thumbnail should promise.
1262
+
1263
+ A hero that fails to build is simply absent, so the client must treat a
1264
+ missing key as ordinary and keep its placeholder.
1265
+ """
1266
+
1267
+ model_config = _RESPONSE_CFG
1268
+
1269
+ sparklines: dict[str, list[float]]
1270
+
1271
+
1272
+ # ======================================================================
1273
+ # Meta / health
1274
+ # ======================================================================
1275
+
1276
+ class StyleResponse(BaseModel):
1277
+ """``GET /v1/meta/style`` -- the house plot style from ``aggregate.style``.
1278
+
1279
+ Served so the SPA's interactive charts and the server-rendered matplotlib
1280
+ plots share one source for their look. ``colors`` is the ``axes.prop_cycle``
1281
+ color list, in order; ``fig_w`` / ``fig_h`` are the house per-panel figure
1282
+ size in inches (``aggregate.constants.FIG_W`` / ``FIG_H``), whose ratio the
1283
+ SPA uses to shape its own panels.
1284
+ """
1285
+
1286
+ model_config = _RESPONSE_CFG
1287
+
1288
+ colors: list[str]
1289
+ grid_color: str
1290
+ text_color: str
1291
+ line_width: float
1292
+ font_size: float
1293
+ fig_w: float
1294
+ fig_h: float
1295
+
1296
+
1297
+ class HealthResponse(BaseModel):
1298
+ model_config = _RESPONSE_CFG
1299
+
1300
+ ok: bool
1301
+ # ``version`` is this api package's version; ``aggregate_version``
1302
+ # is the wrapped library's. Reported separately so a deploy can be
1303
+ # pinned/debugged against both.
1304
+ version: str
1305
+ aggregate_version: str
1306
+
1307
+
1308
+ class PluginLeafInfo(BaseModel):
1309
+ """One document a plugin contributes, with the strings the plugin authored.
1310
+
1311
+ Mirrors :class:`aggregate.plugins.PluginLeaf`. The labels ride on the plugin
1312
+ rather than on the chart or exhibit registry entry, because a navigation hint
1313
+ is a client concern and a registry entry is an IR one.
1314
+ """
1315
+
1316
+ model_config = _RESPONSE_CFG
1317
+
1318
+ name: str
1319
+ kind: str
1320
+ label: str
1321
+ hint: str = ""
1322
+ why: str = ""
1323
+
1324
+
1325
+ class PluginInfo(BaseModel):
1326
+ """One discovered plugin, whether or not it loaded.
1327
+
1328
+ Notes
1329
+ -----
1330
+ ``error`` is **one line**, not a traceback. The traceback goes to the server
1331
+ log; what a reader needs from the About panel is that a plugin failed and
1332
+ roughly why. Meta already carries five other facts and must not become the
1333
+ place tracebacks accumulate. A failed plugin does have to appear *somewhere*
1334
+ a human looks, or an empty Lab tab becomes a debugging session.
1335
+ """
1336
+
1337
+ model_config = _RESPONSE_CFG
1338
+
1339
+ name: str
1340
+ version: str | None = None
1341
+ source: str
1342
+ leaves: list[PluginLeafInfo] = []
1343
+ error: str | None = None
1344
+
1345
+
1346
+ class MetaResponse(BaseModel):
1347
+ model_config = _RESPONSE_CFG
1348
+
1349
+ version: str
1350
+ aggregate_version: str
1351
+ # The static-table engine. Reported because it is a *front-end* version as
1352
+ # much as a backend one: the same install serves the walker the SPA loads
1353
+ # from /v1/assets, so this one number covers both halves.
1354
+ tables_version: str
1355
+ log2_cap: int
1356
+ log2_default: int
1357
+ build_timeout_s: float
1358
+ cache_max: int
1359
+ # Third-party charts and exhibits, and the tab they land under. On meta
1360
+ # rather than on a /v1/plugins route of its own because the SPA already
1361
+ # fetches meta unconditionally at boot, on the very path that has to resolve
1362
+ # before the tab strip can know whether Lab exists; a second route would buy
1363
+ # a second round trip to carry a handful of strings. Empty on a stock
1364
+ # install, which is what suppresses the tab entirely.
1365
+ plugins: list[PluginInfo] = []
1366
+
1367
+
1368
+ class StatusResponse(BaseModel):
1369
+ """The operator's view of the process, behind the private gate.
1370
+
1371
+ Notes
1372
+ -----
1373
+ **The blocks are typed as dicts, and that is a decision rather than a
1374
+ shortcut.** Every other response model here describes a contract the SPA
1375
+ depends on, so ``extra="forbid"`` on a fully declared field set is what
1376
+ stops a typo shipping. This payload has one consumer, the page in
1377
+ ``status_page.html``, which ships in the same commit as the route and reads
1378
+ what it is given. Declaring forty nested models would freeze the shape of
1379
+ an instrument that is expected to grow a panel whenever something new is
1380
+ worth watching, and would put every future panel behind a schema edit for
1381
+ no reader's benefit.
1382
+
1383
+ What *is* declared is the frame the page relies on: the block names, so a
1384
+ panel cannot silently vanish, and ``generated_in_ms``, which is the payload
1385
+ reporting its own cost so a future regression is self evident.
1386
+
1387
+ ``sessions`` and ``key_scope`` are always present. If the session isolation
1388
+ work were ever reverted they would carry ``{"unavailable": "<reason>"}``
1389
+ rather than being dropped, so a missing panel is always a stated fact and
1390
+ never an absent key.
1391
+ """
1392
+
1393
+ model_config = _RESPONSE_CFG
1394
+
1395
+ generated_at: str
1396
+ generated_in_ms: float
1397
+ identity: dict[str, Any]
1398
+ settings: dict[str, Any]
1399
+ sessions: dict[str, Any]
1400
+ cache: dict[str, Any]
1401
+ chart_cache: dict[str, Any]
1402
+ exhibit_cache: dict[str, Any]
1403
+ builds: dict[str, Any]
1404
+ key_scope: dict[str, Any]
1405
+ resources: dict[str, Any]
1406
+ gate: dict[str, Any]
1407
+ watch: dict[str, Any]