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/tables.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Static tables as a semantic document, not as markup.
|
|
2
|
+
|
|
3
|
+
The api's JSON wire format is lossy for presentation. ``serializers.py``
|
|
4
|
+
flattens MultiIndex columns to dotted strings and resets the index into
|
|
5
|
+
ordinary data columns, which is right for a grid and wrong for a printed
|
|
6
|
+
exhibit: a portfolio's ``tail_df`` carries a two level row index, so the unit
|
|
7
|
+
name would be reprinted on all ten of its return-period rows.
|
|
8
|
+
|
|
9
|
+
So the static path does not go through that wire format at all. It hands the
|
|
10
|
+
real DataFrame to ``greater_tables``, which returns a **table document**: an
|
|
11
|
+
``ir_version`` 1 JSON structure carrying dtypes, resolved formats, hierarchy,
|
|
12
|
+
spans and flags, and carrying no widths and no CSS. The browser owns geometry.
|
|
13
|
+
A walker shipped in the same package renders it, so the renderer and the
|
|
14
|
+
document can never version skew.
|
|
15
|
+
|
|
16
|
+
Notes
|
|
17
|
+
-----
|
|
18
|
+
This replaces an evaluation path (``greater_tables`` 5.x) that returned an html
|
|
19
|
+
blob and stamped row emphasis onto it with a positional BeautifulSoup pass. The
|
|
20
|
+
emphasis now rides in the document as ``row_flags``, which is what deletes that
|
|
21
|
+
pass rather than tidying it. See ``dev/plan-gt2-ir.md``.
|
|
22
|
+
|
|
23
|
+
Row flags use the IR's own vocabulary (total / subtotal / emphasis / muted)
|
|
24
|
+
rather than reproducing the old css class names, because the vocabulary happens
|
|
25
|
+
to fit: a portfolio's ``total`` row is a total, a unit's ``Agg`` line is that
|
|
26
|
+
unit's subtotal, and the capital anchors are the only rows left wanting plain
|
|
27
|
+
emphasis. Styling those is the SPA's business, which is the separation the whole
|
|
28
|
+
exercise is about.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
from typing import Any, Callable, Sequence
|
|
35
|
+
|
|
36
|
+
import pandas as pd
|
|
37
|
+
from greater_tables import TableSpec, build, canonical_json
|
|
38
|
+
|
|
39
|
+
# Truncation ceiling, well above any frame that belongs in a static exhibit.
|
|
40
|
+
# Unlike the 5.x path this does not refuse: ``build`` slices to the cap and
|
|
41
|
+
# appends a note saying so, before doing any formatting work, so even a 65,536
|
|
42
|
+
# row density frame is cheap to ask for. The SPA still routes big frames to the
|
|
43
|
+
# grid, which is the honest answer for them, but a direct request degrades
|
|
44
|
+
# rather than erroring.
|
|
45
|
+
MAX_ROWS = 500
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def level_value(df: pd.DataFrame, row: int, name: str) -> Any:
|
|
49
|
+
"""Value of index level ``name`` in row ``row``, or None when absent.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
df : pandas.DataFrame
|
|
54
|
+
Frame whose index is being read.
|
|
55
|
+
row : int
|
|
56
|
+
Positional row number.
|
|
57
|
+
name : str
|
|
58
|
+
Index level name, e.g. ``'unit'``, ``'X'``, ``'T'``.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
Any or None
|
|
63
|
+
None when the frame has no such level, so a caller can write one
|
|
64
|
+
predicate that works for both the Aggregate and the Portfolio shape of
|
|
65
|
+
a frame. An Aggregate's ``tail_df`` is indexed by ``T`` alone; the
|
|
66
|
+
Portfolio's by ``(unit, T)``.
|
|
67
|
+
"""
|
|
68
|
+
names = list(df.index.names or [])
|
|
69
|
+
if name not in names:
|
|
70
|
+
return None
|
|
71
|
+
label = df.index[row]
|
|
72
|
+
if df.index.nlevels == 1:
|
|
73
|
+
return label
|
|
74
|
+
return label[names.index(name)]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# `_is_total`, `_is_anchor`, `_summary_flags` and `_tail_flags` came out at
|
|
78
|
+
# a71 with the ROW_FLAGS entries they served. Between them they decided which
|
|
79
|
+
# of the library's rows a reader should look at hardest, off a hard-coded
|
|
80
|
+
# 1-in-200 and 1-in-250. See ROW_FLAGS below.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
#: Per-frame row emphasis. Frames absent from this map build unflagged.
|
|
84
|
+
#:
|
|
85
|
+
#: **Empty since a71, and it should stay that way.** It held `summary`, `tail_df`
|
|
86
|
+
#: and `reins_summary_df`, deciding which of the library's rows carry weight: the
|
|
87
|
+
#: total, the subtotals, and the two capital anchors on the return-period ladder.
|
|
88
|
+
#: Every one of those tables is a published exhibit that ships its own flags, and
|
|
89
|
+
#: the anchors are the clearest case of why this was the wrong place to hold it:
|
|
90
|
+
#: which return periods a book is capitalized at is the library's choice, this
|
|
91
|
+
#: repo had 1-in-200 and 1-in-250 written down as a fact about it, and the copy
|
|
92
|
+
#: had already gone stale by a68 without anybody noticing.
|
|
93
|
+
#:
|
|
94
|
+
#: The mechanism stays because `frame_document` takes a key and the grid audit
|
|
95
|
+
#: still goes through it. Nothing should be added here for a frame the library
|
|
96
|
+
#: publishes an exhibit for; that is what the exhibit is.
|
|
97
|
+
ROW_FLAGS: dict[str, Callable[[pd.DataFrame, int], Sequence[str]]] = {}
|
|
98
|
+
|
|
99
|
+
#: Per-frame column formats, for the columns whose dtype does not say enough.
|
|
100
|
+
#:
|
|
101
|
+
#: A loss ratio and a return on capital are both just floats, so the engine has
|
|
102
|
+
#: no way to know they should read as percents. These say so once, in the
|
|
103
|
+
#: document, and **both** views then render from the same resolved format: the
|
|
104
|
+
#: walker draws it, and ``irToGridInput`` maps it into the grid's own format-spec
|
|
105
|
+
#: language. The strings are the same ones the SPA used to carry in three
|
|
106
|
+
#: hand-written maps, which is what this replaces.
|
|
107
|
+
#:
|
|
108
|
+
#: A value is either a mapping of column name to spec, or a single spec that
|
|
109
|
+
#: applies to **every** column. The second form was for a frame whose columns
|
|
110
|
+
#: are not statistics: the sharpen score grid's were steps in log2, so one
|
|
111
|
+
#: spec covered the lot.
|
|
112
|
+
#:
|
|
113
|
+
#: Money, everywhere money appears. Grouped, and to the cent.
|
|
114
|
+
#:
|
|
115
|
+
#: It was ``,d`` through a50, and on a real book that made the whole pricing
|
|
116
|
+
#: table integers: every column of the pentagon is money except the three
|
|
117
|
+
#: ratios, `P` was declared ``,d`` outright, and `L`, `M`, `Q` and `a` fell
|
|
118
|
+
#: through to greater-tables' inference, which drops to zero decimals once a
|
|
119
|
+
#: column's mean reaches 20,000 (``engine/formats.py``). So a book priced in the
|
|
120
|
+
#: millions reported its margin as a whole number of dollars and its loss ratio
|
|
121
|
+
#: to a tenth of a percent, which is the wrong way round: the margin is the
|
|
122
|
+
#: small difference between two large numbers and is exactly where the digits
|
|
123
|
+
#: are worth having.
|
|
124
|
+
#:
|
|
125
|
+
#: Two decimals rather than a scale-aware choice. Money is money at every
|
|
126
|
+
#: magnitude, and a table whose decimal count moves with the book is harder to
|
|
127
|
+
#: read across than one that is slightly over-precise in places.
|
|
128
|
+
MONEY = ",.2f"
|
|
129
|
+
|
|
130
|
+
# `PROBABILITY` came out at a71 with the `tail_df` entry that was its only user.
|
|
131
|
+
|
|
132
|
+
FORMATS: dict[str, dict[str, object] | str] = {
|
|
133
|
+
# ---- the Quick Re quote sheet -----------------------------------------
|
|
134
|
+
#
|
|
135
|
+
# The one legitimate entry, and the reason the map still exists. This frame
|
|
136
|
+
# is **this repo's own**, built in `routes.objects.post_reins` out of
|
|
137
|
+
# `layer_pricing.layer_quotes`, so no library exhibit ships its formats and
|
|
138
|
+
# dtype inference is the only alternative. Inference reads `ROL` and `LR`
|
|
139
|
+
# as plain floats and the five currency columns at whatever precision their
|
|
140
|
+
# magnitude suggests, which is exactly the "a table whose decimal count
|
|
141
|
+
# moves with the book" problem `MONEY` exists to stop.
|
|
142
|
+
"reins_quotes": {
|
|
143
|
+
"EL": MONEY,
|
|
144
|
+
"SD load": MONEY,
|
|
145
|
+
"PH": MONEY,
|
|
146
|
+
"Dual": MONEY,
|
|
147
|
+
"Min ROL": MONEY,
|
|
148
|
+
"Premium": MONEY,
|
|
149
|
+
# `LR` takes the library's own ratio reading, `.1%`. **`ROL` does not**,
|
|
150
|
+
# and the extra place is load bearing: `layer_pricing.ROL_FIGS` rounds a
|
|
151
|
+
# rate-on-line quote to four significant figures precisely so the clause
|
|
152
|
+
# and this sheet agree to the digit, and `.1%` prints a rate of 0.1217
|
|
153
|
+
# as `12.2%` beside a clause reading `rol 0.1217`. Three places never
|
|
154
|
+
# hide a figure the clause carries, across both decades a rate on line
|
|
155
|
+
# occupies: `12.170%` on a working layer and `1.217%` at the top. The
|
|
156
|
+
# trailing zero is honest about where the rounding fell.
|
|
157
|
+
"ROL": ".3%",
|
|
158
|
+
"LR": ".1%",
|
|
159
|
+
# A multiple, not a percentage, which is why `CV` departs from the
|
|
160
|
+
# library's `ratio` reading of the same name.
|
|
161
|
+
"CV": ".3f",
|
|
162
|
+
"Span": "s",
|
|
163
|
+
"Binds": "s",
|
|
164
|
+
},
|
|
165
|
+
# ---- otherwise empty, and staying that way ----------------------------
|
|
166
|
+
#
|
|
167
|
+
# Every entry this map ever held named the formats for a frame whose table
|
|
168
|
+
# the library publishes as an exhibit, which ships its formats resolved, so
|
|
169
|
+
# each was this repo asserting how the library's own numbers print.
|
|
170
|
+
# `summary`, `tail_df`, `validation_df`, `reins_summary_df` and
|
|
171
|
+
# `bs_window_df` came out at a71; the six pricing sets (`price`,
|
|
172
|
+
# `reins_price` and the four `stat_*` slices) at a85; the last two,
|
|
173
|
+
# `sharpen_df` and `sharpen_score`, at a94 when the Sharpen leaf moved
|
|
174
|
+
# onto the `sharpen` exhibit the library registered at 1.0.0a255.
|
|
175
|
+
#
|
|
176
|
+
# The `frame/{which}` route still serves the frames for direct api use; it
|
|
177
|
+
# renders them by dtype inference, and if that reads badly the answer is to
|
|
178
|
+
# fetch the exhibit, which is what the app does. Nothing belongs here for a
|
|
179
|
+
# frame the library publishes an exhibit for; that is what the exhibit is.
|
|
180
|
+
# A frame this repo builds itself is the exception, which is what the entry
|
|
181
|
+
# above is: there is no exhibit to fetch.
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def frame_spec(
|
|
186
|
+
df: pd.DataFrame, which: str | None = None, formats: str | None = None,
|
|
187
|
+
) -> TableSpec:
|
|
188
|
+
"""The build spec for one named frame.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
df : pandas.DataFrame
|
|
193
|
+
The frame, needed here because the row-flag predicates read its index
|
|
194
|
+
rather than the row values ``TableSpec`` would otherwise hand them.
|
|
195
|
+
which : str, optional
|
|
196
|
+
Frame name, used to look up row emphasis in ``ROW_FLAGS``. Unknown and
|
|
197
|
+
missing names build unflagged.
|
|
198
|
+
formats : str, optional
|
|
199
|
+
Key into ``FORMATS``. Separate from ``which`` because several frames
|
|
200
|
+
share one format set: the four per-distortion slices are all priced the
|
|
201
|
+
same way.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
TableSpec
|
|
206
|
+
Notes
|
|
207
|
+
-----
|
|
208
|
+
``include_raw`` carries the unrounded value beside the formatted text,
|
|
209
|
+
which is what lets the interactive grid sort and filter on real numbers.
|
|
210
|
+
``irToGridInput`` refuses a document built without it.
|
|
211
|
+
|
|
212
|
+
It is the **explicit column list**, not the ``'data'`` shorthand the
|
|
213
|
+
handoff spec names, and the difference is load bearing: ``'data'`` means
|
|
214
|
+
numeric, date and bool columns only, so a string data column gets no raw
|
|
215
|
+
value and the adapter then throws on the whole document. Three of the
|
|
216
|
+
Price tab's frames carry one (``distortion``, ``param_name``). Naming
|
|
217
|
+
every column is what makes the two features compose. Reported upstream;
|
|
218
|
+
see ``dev/TODO.md``.
|
|
219
|
+
|
|
220
|
+
Columns absent from a ``FORMATS`` entry are left to the engine, which
|
|
221
|
+
infers from the dtype. Only the ones whose meaning outruns their dtype
|
|
222
|
+
need naming.
|
|
223
|
+
|
|
224
|
+
There is no ``full_precision`` here since a68. It rebuilt the document
|
|
225
|
+
with ``formatters={}`` and a wide ``float_format``, which was only ever
|
|
226
|
+
a way to reprint numbers a client could not reach; ``include_raw`` puts
|
|
227
|
+
them in every document, so reprinting is the client's to do and costs no
|
|
228
|
+
round trip. See ``dev/plan-ui-round-5.md``.
|
|
229
|
+
"""
|
|
230
|
+
flags = ROW_FLAGS.get(which or "")
|
|
231
|
+
chosen = FORMATS.get(formats or "")
|
|
232
|
+
if isinstance(chosen, str):
|
|
233
|
+
columns = {c: chosen for c in df.columns}
|
|
234
|
+
else:
|
|
235
|
+
# Filter to what the frame actually has: an Aggregate's pentagon carries
|
|
236
|
+
# fewer columns than a Portfolio's, and naming an absent one is not an
|
|
237
|
+
# error worth raising.
|
|
238
|
+
#
|
|
239
|
+
# Under a **spanned** header the name to match is the innermost level,
|
|
240
|
+
# not the whole tuple. `x in df.columns` on a MultiIndex tests the first
|
|
241
|
+
# level, so a frame whose columns are (component, measure) matched none
|
|
242
|
+
# of the measure names declared above and quietly fell through to
|
|
243
|
+
# inference for the whole table. Building the key set from the last
|
|
244
|
+
# level and mapping back to the full tuples is what makes one
|
|
245
|
+
# declaration cover `('freq', 'mean')`, `('sev', 'mean')` and
|
|
246
|
+
# `('agg', 'mean')`, which is right: the format belongs to the measure.
|
|
247
|
+
wanted = chosen or {}
|
|
248
|
+
if isinstance(df.columns, pd.MultiIndex):
|
|
249
|
+
columns = {
|
|
250
|
+
col: wanted[col[-1]] for col in df.columns if col[-1] in wanted
|
|
251
|
+
}
|
|
252
|
+
else:
|
|
253
|
+
columns = {k: v for k, v in wanted.items() if k in df.columns}
|
|
254
|
+
return TableSpec(
|
|
255
|
+
include_raw=list(df.columns),
|
|
256
|
+
max_rows=MAX_ROWS,
|
|
257
|
+
row_flags=(lambda pos, _row: flags(df, pos)) if flags else None,
|
|
258
|
+
# ``formatters``, not ``formats``: the field was renamed somewhere in the
|
|
259
|
+
# ``greater_tables`` 1.9 to 6.0.0a4 run that the sibling checkout has
|
|
260
|
+
# moved through. Following the rename is all a37 does about that move;
|
|
261
|
+
# catching up with the rest of 6.0 is its own piece of work.
|
|
262
|
+
formatters=columns,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def frame_document(
|
|
267
|
+
df: pd.DataFrame, which: str | None = None, formats: str | None = None,
|
|
268
|
+
) -> tuple[bytes, str]:
|
|
269
|
+
"""Render a frame as canonical table-document JSON.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
df : pandas.DataFrame
|
|
274
|
+
The frame, **with its index intact**. Do not ``reset_index()`` first:
|
|
275
|
+
the row index is what gets sparsified into stub rowspans, and flattening
|
|
276
|
+
it into data columns throws away the whole benefit.
|
|
277
|
+
which : str, optional
|
|
278
|
+
Frame name, for row emphasis. See ``ROW_FLAGS``.
|
|
279
|
+
formats : str, optional
|
|
280
|
+
Format-set key. See ``FORMATS``.
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
body : bytes
|
|
285
|
+
Deterministic UTF-8 JSON: the same frame and spec give byte identical
|
|
286
|
+
output, which is what makes the content hash usable as an ETag.
|
|
287
|
+
hash : str
|
|
288
|
+
The document's 12-hex content hash, returned alongside so a caller can
|
|
289
|
+
set an ETag without parsing the body back.
|
|
290
|
+
|
|
291
|
+
Raises
|
|
292
|
+
------
|
|
293
|
+
ValueError
|
|
294
|
+
If the frame is empty or carries duplicate column names.
|
|
295
|
+
"""
|
|
296
|
+
if df is None or df.empty:
|
|
297
|
+
raise ValueError("nothing to render: the frame is empty")
|
|
298
|
+
if not df.columns.is_unique:
|
|
299
|
+
# Naming the frame here beats surfacing a library error the caller
|
|
300
|
+
# cannot place.
|
|
301
|
+
raise ValueError("frame has duplicate column names")
|
|
302
|
+
doc = build(df, frame_spec(df, which, formats))
|
|
303
|
+
return canonical_json(doc), doc.hash
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def frame_document_dict(
|
|
307
|
+
df: pd.DataFrame, which: str | None = None, formats: str | None = None
|
|
308
|
+
) -> dict:
|
|
309
|
+
"""``frame_document`` as a parsed object, for embedding in a JSON response.
|
|
310
|
+
|
|
311
|
+
The frame routes return the canonical bytes directly, because that is what
|
|
312
|
+
the ETag hashes. The POST pricing endpoints carry their documents *inside* a
|
|
313
|
+
Pydantic response, so those need the parsed form.
|
|
314
|
+
"""
|
|
315
|
+
body, _hash = frame_document(df, which, formats)
|
|
316
|
+
return json.loads(body)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aggregate_api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: aggregate Loss Lab (aLL): FastAPI service and single-page web app for the aggregate actuarial library.
|
|
5
|
+
Author-email: "Stephen J. Mildenhall" <steve@convexrisk.com>
|
|
6
|
+
Maintainer-email: "Stephen J. Mildenhall" <steve@convexrisk.com>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/mynl/aggregate_api
|
|
9
|
+
Project-URL: Source Code, https://github.com/mynl/aggregate_api
|
|
10
|
+
Project-URL: Documentation, https://github.com/mynl/aggregate_api#readme
|
|
11
|
+
Project-URL: Changelog, https://github.com/mynl/aggregate_api/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Issues, https://github.com/mynl/aggregate_api/issues
|
|
13
|
+
Keywords: actuarial,insurance,reinsurance,risk,aggregate loss,compound distribution,FFT,pricing,DecL
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
21
|
+
Classifier: Framework :: FastAPI
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: aggregate<2,>=1.0.1
|
|
26
|
+
Requires-Dist: greater-tables>=6.0.0
|
|
27
|
+
Requires-Dist: fastapi>=0.115
|
|
28
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
29
|
+
Requires-Dist: pydantic>=2.7
|
|
30
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
33
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
35
|
+
Provides-Extra: status
|
|
36
|
+
Requires-Dist: psutil>=5.9; extra == "status"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# aggregate_api
|
|
40
|
+
|
|
41
|
+
**aggregate Loss Lab** (aLL): a FastAPI service and a single-page web app for
|
|
42
|
+
the [`aggregate`](https://github.com/mynl/aggregate) actuarial library.
|
|
43
|
+
*Description to distribution.*
|
|
44
|
+
|
|
45
|
+
`aggregate_api` is the package; **aggregate Loss Lab** is the app it serves.
|
|
46
|
+
It puts `build()` behind an HTTP/JSON api (DecL parsing, FFT-based compound
|
|
47
|
+
distributions, plotting, and risk pricing) and ships a Bootstrap 5 and
|
|
48
|
+
CodeMirror 6 DecL workbench that runs against it. A single `aggregate-api`
|
|
49
|
+
process serves both the web UI (at `/`) and the JSON endpoints (under `/v1`)
|
|
50
|
+
same-origin.
|
|
51
|
+
|
|
52
|
+
> **Status:** 1.0.0, the first public release. Extracted from the `aggregate`
|
|
53
|
+
> repo so the library can ship without it. The package is installable and
|
|
54
|
+
> supported; the `/v1` surface is not yet a frozen interface, and the caveat at
|
|
55
|
+
> the end of [Running headless](#running-headless) says why. See
|
|
56
|
+
> [CHANGELOG.md](CHANGELOG.md) for what has landed and
|
|
57
|
+
> [dev/TODO.md](dev/TODO.md) for the roadmap.
|
|
58
|
+
|
|
59
|
+
## What's inside
|
|
60
|
+
|
|
61
|
+
- **Backend** (`src/aggregate_api/`): FastAPI app. Object lifecycle
|
|
62
|
+
(`POST /v1/objects` to build and cache, then `info`, `meta`, `summary`,
|
|
63
|
+
`tail_df`, `validation_df`, `stats_df`, `density_df`, `plot`, `kappa`,
|
|
64
|
+
`price`, `pricing_at`, plus `frame/{which}` in CSV or table-document form),
|
|
65
|
+
DecL helpers (`/v1/decl/complete`, `/lex`,
|
|
66
|
+
`/format`), and the example library read straight from `aggregate`'s recipe
|
|
67
|
+
base (`/v1/examples`, `/v1/examples/heroes`). In-memory LRU cache, build
|
|
68
|
+
timeout, SQLite audit log, CORS. Swagger UI at `/docs`.
|
|
69
|
+
- **Frontend** (`web/`): vanilla-JS SPA (Vite, Bootstrap, CodeMirror 6,
|
|
70
|
+
`csv-grid`). A DecL editor with syntax highlighting, autocomplete and history,
|
|
71
|
+
a landing gallery of showcase examples, tabbed risk output, interactive and
|
|
72
|
+
native plots, and a rich parse-error pane.
|
|
73
|
+
|
|
74
|
+
### Tables: one payload, two renderers
|
|
75
|
+
|
|
76
|
+
Every table under 500 rows is fetched once, as a **table document**: versioned
|
|
77
|
+
JSON carrying semantics only (dtypes, resolved formats, hierarchy, spans, flags)
|
|
78
|
+
and no widths or CSS. Both views render from that one document, so they cannot
|
|
79
|
+
disagree about a number:
|
|
80
|
+
|
|
81
|
+
- **Static** is `greater_tables`' own JS walker, which draws a book-quality table
|
|
82
|
+
(sparsified row index, spanned headers, partial rules). The walker and its
|
|
83
|
+
stylesheet are served straight out of the installed Python package at
|
|
84
|
+
`/v1/assets/`, so the renderer and the documents it renders can never version
|
|
85
|
+
skew.
|
|
86
|
+
- **Interactive** is `csv-grid`, fed by `irToGridInput(doc)`: sort, per-column
|
|
87
|
+
filter, fzf search, copy and save.
|
|
88
|
+
|
|
89
|
+
Which one you get is a page-wide preference in the header menu. Large frames (the
|
|
90
|
+
densities) skip the document entirely and go to the grid, which is the honest
|
|
91
|
+
instrument for them.
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
Requires Python ≥ 3.13. Nothing else: `aggregate` ≥ 1.0.1 and
|
|
96
|
+
`greater-tables` ≥ 6.0.0 come from PyPI with it.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
pip install aggregate_api
|
|
100
|
+
aggregate-api --port 8001
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then open http://127.0.0.1:8001/ for the app, or http://127.0.0.1:8001/docs for
|
|
104
|
+
the api.
|
|
105
|
+
|
|
106
|
+
> **Install the wheel, not the repository.** The web bundle is 2.5 MB of
|
|
107
|
+
> generated Vite output and is deliberately not in git, so it reaches you in the
|
|
108
|
+
> built distribution and only there. `pip install git+https://github.com/mynl/aggregate_api`
|
|
109
|
+
> installs a working api whose `/` returns 404, which is a reasonable thing to
|
|
110
|
+
> want if you are putting your own front end in front of the engine, and a
|
|
111
|
+
> baffling one otherwise. The wheel and sdist are also attached to each
|
|
112
|
+
> [GitHub release](https://github.com/mynl/aggregate_api/releases).
|
|
113
|
+
|
|
114
|
+
One footnote, which the pins above handle for you and which bites anyone
|
|
115
|
+
installing the table engine on its own: PyPI's `greater-tables` 5.3 is a
|
|
116
|
+
previous generation sharing the `greater_tables` import name, so a bare
|
|
117
|
+
`pip install greater-tables` can get a package with no `build`, `canonical_json`
|
|
118
|
+
or `IR_VERSION`, which fails at import rather than at install. This package pins
|
|
119
|
+
`>=6.0.0` and `tests/test_meta.py` asserts the right generation is present.
|
|
120
|
+
|
|
121
|
+
## Develop
|
|
122
|
+
|
|
123
|
+
Needs [`uv`](https://docs.astral.sh/uv/) and Node for the web build. A fresh
|
|
124
|
+
clone syncs with nothing checked out beside it.
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
git clone https://github.com/mynl/aggregate_api
|
|
128
|
+
cd aggregate_api
|
|
129
|
+
uv sync --extra dev
|
|
130
|
+
.\scripts\build-web.ps1 # Windows; ./scripts/build-web.sh on Unix
|
|
131
|
+
uv run aggregate-api --port 8001 --reload
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`build-web` drops the bundle into `src/aggregate_api/static/`, which the FastAPI
|
|
135
|
+
`StaticFiles` mount serves at `/`. Until it has run once, `/` has nothing to
|
|
136
|
+
serve. For hot reload with a Vite dev server proxying `/v1` to `:8000`, use
|
|
137
|
+
`cd web; npm install; npm run dev` instead.
|
|
138
|
+
|
|
139
|
+
To develop against a local checkout of `aggregate` or `greater-tables` rather
|
|
140
|
+
than the released build, install it editable over the synced environment:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
uv pip install -e ../aggregate # or wherever the checkout is
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The next `uv sync` silently reinstalls the released build, so `uv run --no-sync`
|
|
147
|
+
is the way to work in between.
|
|
148
|
+
|
|
149
|
+
Run the tests. There are two suites and both are the gate:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
uv run pytest # 490 tests, FastAPI TestClient, no live server needed
|
|
153
|
+
cd web; npm test # 277 tests, node --test over web/test/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Rehearse a release, which is a stronger check than either suite because it
|
|
157
|
+
builds the distributions and runs everything against the installed wheel in a
|
|
158
|
+
clean environment:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
.\scripts\release-check.ps1
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Running headless
|
|
165
|
+
|
|
166
|
+
One process serves both halves: the routers mount under `/v1`, and the built web
|
|
167
|
+
bundle is mounted at `/`. To run the engine alone, behind somebody else's front
|
|
168
|
+
end, say so:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
uv run aggregate-api --headless # or AGGAPI_SERVE_SPA=0
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`/v1`, `/docs` and `/openapi.json` all stay; only `/` stops answering. A front
|
|
175
|
+
end on another origin also wants `AGGAPI_CORS_ORIGINS`, comma separated:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
AGGAPI_CORS_ORIGINS=https://your.app uv run aggregate-api --headless
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`/openapi.json` is the contract. Note that the `/v1` surface is pre-1.0 and its
|
|
182
|
+
shape follows what the bundled web app needs, so treat it as a moving target
|
|
183
|
+
rather than a stable interface for now.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
aggregate_api/__init__.py,sha256=k656-0vnCmNakINSCiCW3q7HlYswEdSV7LyyNjhK_vU,1340
|
|
2
|
+
aggregate_api/__main__.py,sha256=h68CVF82mMY3TlRFCRHQJfMzs3Rd5E4UlF_dBP2jQKA,5672
|
|
3
|
+
aggregate_api/app.py,sha256=TxXHw1MFE603fRz9VoEZgVffbud9wozZ2lH2HX3GmWc,8963
|
|
4
|
+
aggregate_api/audit.py,sha256=wlBpeoy7nZXQID_KGp5HF0XJUYaY9MhZ05YMioCCDtQ,17186
|
|
5
|
+
aggregate_api/bounds.py,sha256=jnNsaxBZTHEinUGDpX3xnO770JurYhCJkMbKQAxD20Y,14753
|
|
6
|
+
aggregate_api/cache.py,sha256=vEIUGko1esJxg0ixHilFZgeqnTfMjFxSRT-CzVkohOQ,12308
|
|
7
|
+
aggregate_api/capability.py,sha256=Q52stFJlKiwVozsCIYgglhaKQalRfOPMID9JDD89kf8,31640
|
|
8
|
+
aggregate_api/completion.py,sha256=dETpa3c9yDBI1fjFwPGyFTpFg6GzHPoBAsL8OnfAiy8,8506
|
|
9
|
+
aggregate_api/config.py,sha256=Q3tgSNG9ebGJjuqcWAo8dnPpPxKBCGzyyZMpXzngSqo,16936
|
|
10
|
+
aggregate_api/cors.py,sha256=fYmGBtpM6tjRctzrA55UXkOMweMFT-prh-2FUjBQ1NU,2537
|
|
11
|
+
aggregate_api/examples.py,sha256=XEWmBwXz77HgYPPDs3xocT7T4rk3Q62muaEy0LhfkFI,27243
|
|
12
|
+
aggregate_api/layer_pricing.py,sha256=wCKulSUVaXUmBjauuTeum2x9odsSo-Jz_DtXyAaMVis,36623
|
|
13
|
+
aggregate_api/library.py,sha256=sYZGqsYV2_d6p3-V54HJSrnjD9dzSugXYyfvER6a_v4,3893
|
|
14
|
+
aggregate_api/library_notes.py,sha256=pB_vPWxM6qVqdppMpi7084IIrdC4slIHAcRmR6dgjYE,3676
|
|
15
|
+
aggregate_api/models.py,sha256=rcyOL1OySucmM52qxGTkzopNqzHHXbIT8ZftJ--NNKE,56889
|
|
16
|
+
aggregate_api/net.py,sha256=wIgpd_dLKY7p765e0N_LNwhnmcc9LhqjIYO0aDWHHHM,10981
|
|
17
|
+
aggregate_api/pnl.py,sha256=61OyheKtRSSA-oaiEyh1qU4slh2TpQbpe4uxrIKPhes,4786
|
|
18
|
+
aggregate_api/pricing.py,sha256=8z6Oz0cJzjTK4Pz6fD6L_f8EBhFhQq4lY7L2Dgr_r_4,35382
|
|
19
|
+
aggregate_api/resources.py,sha256=N__uWH1CoIkblgdJrhO6ugcDXFL2Ids4u8wdUyODTs0,10230
|
|
20
|
+
aggregate_api/serializers.py,sha256=4LOoXkF9gMdcuenbhf3xD8aT7Yr610bwkJ6Nv4X-X_s,23941
|
|
21
|
+
aggregate_api/sessions.py,sha256=eHK1Nr7YDX9miT6P8st-eDAVaE5IUOJu4RUo9OfSEU8,14980
|
|
22
|
+
aggregate_api/status.py,sha256=iZfVt8CItp4tYWGUHEO5BLyXtbnMMe6MRuo693VzsR8,21096
|
|
23
|
+
aggregate_api/status_page.html,sha256=TjJP3kkS_fceBU26aU1k9Iq-H_1J-NsHNxAyhGt67OE,22004
|
|
24
|
+
aggregate_api/tables.py,sha256=17mFbsDtJolS6mHrRrvNWWqfp3fJ7bLYI-381zbm394,15005
|
|
25
|
+
aggregate_api/routes/__init__.py,sha256=piuaQpv5J-swGvFwFOP3AqohJ5JUgMJ992o5ErW1pmE,246
|
|
26
|
+
aggregate_api/routes/decl.py,sha256=Rqn34wl84RSaPmYDjNIfN_zQhchIMHpcd3kEFE2q0kQ,14613
|
|
27
|
+
aggregate_api/routes/examples.py,sha256=MBlmm8Hytd8OVpbA7ZaHiLeehDZxsrUu-cuQzCjJ_0Y,3070
|
|
28
|
+
aggregate_api/routes/meta.py,sha256=TiarkXSrQZBequroDU2_BCP7b8H8JNjHDu3jwAMm0KY,11108
|
|
29
|
+
aggregate_api/routes/objects.py,sha256=k6Mn_NfX9GdVlh5ZoOKDVlrVWKLN6-fb4T-aAmk387U,190715
|
|
30
|
+
aggregate_api/routes/status.py,sha256=l1JtQkumLyCvZzwLKMGt6pMqSse_exTcNB5Xt_flF2M,20331
|
|
31
|
+
aggregate_api/static/aggregate-api-logo-512.png,sha256=IPG-GqRdMmzbLiCZ3IHqvi6-ISHV-h17082Clpc-ML0,204132
|
|
32
|
+
aggregate_api/static/aggregate-api-logo.png,sha256=qM71q7DBAwRITWTP8zr0YYj4dVKkz8PW63-YfoV3LFc,909083
|
|
33
|
+
aggregate_api/static/aggregate-api-trim.png,sha256=9VShrxMcovZINKomb6eqf7lDTNRyr53FplcRtWqodyw,439370
|
|
34
|
+
aggregate_api/static/android-chrome-192x192.png,sha256=pXGYAIQCMoszyIW-T3QRZpnhPeEDFiZv_fOzZm1T_LE,28801
|
|
35
|
+
aggregate_api/static/android-chrome-512x512.png,sha256=7qUmwO6yXtRthgp7QE5VDHzesfMHrukbhDEE6_bfQpY,183910
|
|
36
|
+
aggregate_api/static/apple-touch-icon.png,sha256=ZcMULrusE15EVPWy1rtOFAPcpHWFWb-wUzA6gGtkSh8,26298
|
|
37
|
+
aggregate_api/static/favicon-16x16.png,sha256=bBRfwBovSVLijTJlsRgFWPahSZXhmg6n0_RJ24BuCnE,636
|
|
38
|
+
aggregate_api/static/favicon-32x32.png,sha256=Jqw4IO6U4CUnQI_8vMVnsiMJdKM5OsPpPaGzdm3BEnQ,1456
|
|
39
|
+
aggregate_api/static/favicon.ico,sha256=930lTNx16SzUqifmmTjOXsBcwWcn0QHIkEvgM0zb4rA,4917
|
|
40
|
+
aggregate_api/static/index.html,sha256=nZlvRp06EV3riF1NSWX1Ed3yls_ffOa-bmFIMmbGPck,62604
|
|
41
|
+
aggregate_api/static/lite.html,sha256=0E3hLpL3X8aSQrS0XK8m0BqoACzZPY8GDBEUZ3YwBo8,3989
|
|
42
|
+
aggregate_api/static/logo.png,sha256=IPG-GqRdMmzbLiCZ3IHqvi6-ISHV-h17082Clpc-ML0,204132
|
|
43
|
+
aggregate_api/static/site.webmanifest,sha256=FK0rjGM6nniEoY7TA-4xJ2pDHowNZzaHbB0XiIWOYuA,493
|
|
44
|
+
aggregate_api/static/sw.js,sha256=LtKvw7mL3xPtgZWSf18qsnHgGCZJZSD6NWPwrbjPFb0,3405
|
|
45
|
+
aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff,sha256=9VUTt7WRy4SjuH_w406iTUgx1v7cIuVLkRymS1tUShU,180288
|
|
46
|
+
aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2,sha256=bHVxA2ShylYEJncW9tKJl7JjGf2weM8R4LQqtm_y6mE,134044
|
|
47
|
+
aggregate_api/static/assets/bootstrap-ohb1VZ53.js,sha256=cpwBrnRcJshb--DpvmeJZ9hdZdQIqJb3WLJHs4MC-ik,81694
|
|
48
|
+
aggregate_api/static/assets/codemirror-h62DHGGa.js,sha256=I1GK_BwfQTaM0BD0fAXr4Iv-JjFzfoSDkeNlVh2sCdU,351580
|
|
49
|
+
aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js,sha256=wBxTBubbiXUCRZ1MbGQjZfmxZFTbofeN5Pfc3zvvllA,8384
|
|
50
|
+
aggregate_api/static/assets/echarts-B7o9sc00.js,sha256=vkBuu4yHZTSjpPwqc14zOAEv3R_z2wc3oe-LeJKY22s,610652
|
|
51
|
+
aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js,sha256=bQ96Cfn42OoRsDkob8MxBMK9wdkexSvvgZl7TrM_GUE,601841
|
|
52
|
+
aggregate_api/static/assets/lite-CUlcD8p4.css,sha256=Ez8V80fqbLckwr2lBR0BI9OpOg3bbUZw8U6FOlaseKQ,6948
|
|
53
|
+
aggregate_api/static/assets/lite-Dd2TnT4M.js,sha256=_Fprm777SkLY-ScGwkXoUDo398aVJGSKsczt6veLOE8,7458
|
|
54
|
+
aggregate_api/static/assets/main-Bxhxa55v.css,sha256=yRBfKQrTMrpeQ2Cbj7TCapav7VRILGqsoB5hzOo0CmQ,341088
|
|
55
|
+
aggregate_api/static/assets/main-CmoEiPit.js,sha256=gk0M4PM6Cth17t0gOERMJXD-RDvVqRrX3aV5QUTSsqQ,80404
|
|
56
|
+
aggregate_api/static/assets/tables-BHCF7qIF.js,sha256=FUXaqrUx2BEKdXsCASbTWrD5_CmDbKC_3xeal737Cfc,143283
|
|
57
|
+
aggregate_api/static/assets/tables-CxvajLr7.css,sha256=eLbXm2aU617Z0nNf0_eR3Q7kredW2CcGbub4UIKscu0,6671
|
|
58
|
+
aggregate_api-1.0.0.dist-info/licenses/LICENSE,sha256=8rM_5K4tx-FCI8JUCaGUM1RTdEDbzR2nlIjlj_X7GG0,1536
|
|
59
|
+
aggregate_api-1.0.0.dist-info/METADATA,sha256=uktUHsuhLT80J89rCazQK6t-cWB4h5LYcolfA-ehbJA,8083
|
|
60
|
+
aggregate_api-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
61
|
+
aggregate_api-1.0.0.dist-info/entry_points.txt,sha256=Esuykv5feeGM3pFYFcZTBn_C8tGr9PKGYtSx3cLjqRg,62
|
|
62
|
+
aggregate_api-1.0.0.dist-info/top_level.txt,sha256=wWdJVV1WG7P7g0TGiDG1lVck40S-jtSDM6Y1cKq3idI,14
|
|
63
|
+
aggregate_api-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Stephen J. Mildenhall
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aggregate_api
|