aggregate_api 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. aggregate_api/__init__.py +41 -0
  2. aggregate_api/__main__.py +154 -0
  3. aggregate_api/app.py +206 -0
  4. aggregate_api/audit.py +395 -0
  5. aggregate_api/bounds.py +331 -0
  6. aggregate_api/cache.py +319 -0
  7. aggregate_api/capability.py +823 -0
  8. aggregate_api/completion.py +219 -0
  9. aggregate_api/config.py +363 -0
  10. aggregate_api/cors.py +61 -0
  11. aggregate_api/examples.py +620 -0
  12. aggregate_api/layer_pricing.py +840 -0
  13. aggregate_api/library.py +94 -0
  14. aggregate_api/library_notes.py +96 -0
  15. aggregate_api/models.py +1407 -0
  16. aggregate_api/net.py +281 -0
  17. aggregate_api/pnl.py +101 -0
  18. aggregate_api/pricing.py +778 -0
  19. aggregate_api/resources.py +257 -0
  20. aggregate_api/routes/__init__.py +8 -0
  21. aggregate_api/routes/decl.py +327 -0
  22. aggregate_api/routes/examples.py +82 -0
  23. aggregate_api/routes/meta.py +282 -0
  24. aggregate_api/routes/objects.py +4119 -0
  25. aggregate_api/routes/status.py +466 -0
  26. aggregate_api/serializers.py +565 -0
  27. aggregate_api/sessions.py +353 -0
  28. aggregate_api/static/aggregate-api-logo-512.png +0 -0
  29. aggregate_api/static/aggregate-api-logo.png +0 -0
  30. aggregate_api/static/aggregate-api-trim.png +0 -0
  31. aggregate_api/static/android-chrome-192x192.png +0 -0
  32. aggregate_api/static/android-chrome-512x512.png +0 -0
  33. aggregate_api/static/apple-touch-icon.png +0 -0
  34. aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
  35. aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  36. aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
  37. aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
  38. aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
  39. aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
  40. aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
  41. aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
  42. aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
  43. aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
  44. aggregate_api/static/assets/main-CmoEiPit.js +9 -0
  45. aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
  46. aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
  47. aggregate_api/static/favicon-16x16.png +0 -0
  48. aggregate_api/static/favicon-32x32.png +0 -0
  49. aggregate_api/static/favicon.ico +0 -0
  50. aggregate_api/static/index.html +912 -0
  51. aggregate_api/static/lite.html +83 -0
  52. aggregate_api/static/logo.png +0 -0
  53. aggregate_api/static/site.webmanifest +14 -0
  54. aggregate_api/static/sw.js +78 -0
  55. aggregate_api/status.py +536 -0
  56. aggregate_api/status_page.html +546 -0
  57. aggregate_api/tables.py +316 -0
  58. aggregate_api-1.0.0.dist-info/METADATA +187 -0
  59. aggregate_api-1.0.0.dist-info/RECORD +63 -0
  60. aggregate_api-1.0.0.dist-info/WHEEL +5 -0
  61. aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
  62. aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
  63. aggregate_api-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aggregate-api = aggregate_api.__main__:main
@@ -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