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.
- aggregate_api/__init__.py +41 -0
- aggregate_api/__main__.py +154 -0
- aggregate_api/app.py +206 -0
- aggregate_api/audit.py +395 -0
- aggregate_api/bounds.py +331 -0
- aggregate_api/cache.py +319 -0
- aggregate_api/capability.py +823 -0
- aggregate_api/completion.py +219 -0
- aggregate_api/config.py +363 -0
- aggregate_api/cors.py +61 -0
- aggregate_api/examples.py +620 -0
- aggregate_api/layer_pricing.py +840 -0
- aggregate_api/library.py +94 -0
- aggregate_api/library_notes.py +96 -0
- aggregate_api/models.py +1407 -0
- aggregate_api/net.py +281 -0
- aggregate_api/pnl.py +101 -0
- aggregate_api/pricing.py +778 -0
- aggregate_api/resources.py +257 -0
- aggregate_api/routes/__init__.py +8 -0
- aggregate_api/routes/decl.py +327 -0
- aggregate_api/routes/examples.py +82 -0
- aggregate_api/routes/meta.py +282 -0
- aggregate_api/routes/objects.py +4119 -0
- aggregate_api/routes/status.py +466 -0
- aggregate_api/serializers.py +565 -0
- aggregate_api/sessions.py +353 -0
- aggregate_api/static/aggregate-api-logo-512.png +0 -0
- aggregate_api/static/aggregate-api-logo.png +0 -0
- aggregate_api/static/aggregate-api-trim.png +0 -0
- aggregate_api/static/android-chrome-192x192.png +0 -0
- aggregate_api/static/android-chrome-512x512.png +0 -0
- aggregate_api/static/apple-touch-icon.png +0 -0
- aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
- aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
- aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
- aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
- aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
- aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
- aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
- aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
- aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
- aggregate_api/static/assets/main-CmoEiPit.js +9 -0
- aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
- aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
- aggregate_api/static/favicon-16x16.png +0 -0
- aggregate_api/static/favicon-32x32.png +0 -0
- aggregate_api/static/favicon.ico +0 -0
- aggregate_api/static/index.html +912 -0
- aggregate_api/static/lite.html +83 -0
- aggregate_api/static/logo.png +0 -0
- aggregate_api/static/site.webmanifest +14 -0
- aggregate_api/static/sw.js +78 -0
- aggregate_api/status.py +536 -0
- aggregate_api/status_page.html +546 -0
- aggregate_api/tables.py +316 -0
- aggregate_api-1.0.0.dist-info/METADATA +187 -0
- aggregate_api-1.0.0.dist-info/RECORD +63 -0
- aggregate_api-1.0.0.dist-info/WHEEL +5 -0
- aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
- aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
- aggregate_api-1.0.0.dist-info/top_level.txt +1 -0
aggregate_api/models.py
ADDED
|
@@ -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]
|