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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Meta routes: health check, config introspection, and the plot style.
|
|
2
|
+
|
|
3
|
+
Routes
|
|
4
|
+
------
|
|
5
|
+
|
|
6
|
+
* ``GET /v1/health``: liveness probe (``{ok: true, version,
|
|
7
|
+
aggregate_version}``). Used by Caddy / k8s health checks and by
|
|
8
|
+
the SPA's "server up?" splash logic.
|
|
9
|
+
* ``GET /v1/meta``: runtime config (log2_cap, build_timeout, etc.)
|
|
10
|
+
so the SPA can configure its form widgets (e.g. set the log2
|
|
11
|
+
slider's max to ``log2_cap``), plus the loaded plugin manifest,
|
|
12
|
+
which is what tells the SPA whether the Lab tab exists and what
|
|
13
|
+
sits under it.
|
|
14
|
+
* ``GET /v1/meta/style``: the house plot style, read off
|
|
15
|
+
``aggregate.style``, so the SPA's interactive charts and the
|
|
16
|
+
server-rendered matplotlib plots cannot drift apart.
|
|
17
|
+
* ``GET /v1/assets/{name}``: the ``greater_tables`` table-document
|
|
18
|
+
walker and its stylesheet, served out of the installed package so
|
|
19
|
+
the renderer cannot skew from the documents this process emits.
|
|
20
|
+
|
|
21
|
+
These are cheap reads; they don't touch the cache or audit log.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from functools import lru_cache
|
|
27
|
+
from importlib.metadata import version as _pkg_version
|
|
28
|
+
from importlib.resources import files
|
|
29
|
+
|
|
30
|
+
from aggregate import plugins as agg_plugins
|
|
31
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
32
|
+
from fastapi.responses import Response
|
|
33
|
+
|
|
34
|
+
from .. import models
|
|
35
|
+
from ..config import get_settings
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# APIRouter is FastAPI's analogue of a Flask Blueprint -- a group
|
|
39
|
+
# of routes that the app factory mounts at a prefix.
|
|
40
|
+
router = APIRouter()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get("/health", response_model=models.HealthResponse)
|
|
44
|
+
async def health() -> dict:
|
|
45
|
+
"""Trivial liveness check.
|
|
46
|
+
|
|
47
|
+
Returns both the api package version (``version``) and the
|
|
48
|
+
underlying ``aggregate`` library version (``aggregate_version``)
|
|
49
|
+
so a curl-from-prod debugging session can confirm which build of
|
|
50
|
+
each it's talking to without an OpenAPI fetch.
|
|
51
|
+
|
|
52
|
+
``async def``, and with no dependencies, **so that it answers when the
|
|
53
|
+
thread pool is exhausted**. FastAPI runs a sync route in an anyio worker
|
|
54
|
+
thread, which needs one of 40 tokens; a sync route therefore cannot be
|
|
55
|
+
served at all once 40 requests are stuck, which is exactly the moment
|
|
56
|
+
somebody asks whether the server is alive. This route does no blocking work,
|
|
57
|
+
so running it on the event loop costs nothing and removes it from that
|
|
58
|
+
failure entirely. A sync *dependency* would reintroduce the problem, which is
|
|
59
|
+
why there is not one here. See ``_locked_entry`` in ``routes/objects.py`` for
|
|
60
|
+
the failure this protects against.
|
|
61
|
+
"""
|
|
62
|
+
return {
|
|
63
|
+
"ok": True,
|
|
64
|
+
"version": _pkg_version("aggregate_api"),
|
|
65
|
+
"aggregate_version": _pkg_version("aggregate"),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Fallbacks for a style key ``aggregate.style`` does not set. These are the
|
|
70
|
+
# values the shipped style uses today, so a miss degrades to the same look
|
|
71
|
+
# rather than to browser defaults.
|
|
72
|
+
_STYLE_FALLBACK = {
|
|
73
|
+
"colors": ["#0d6efd", "#dc3545", "#198754", "#eaab00",
|
|
74
|
+
"#6f42c1", "#0aa2c0", "#d63384", "#6c757d"],
|
|
75
|
+
"grid_color": "#dee2e6",
|
|
76
|
+
"text_color": "#212529",
|
|
77
|
+
"line_width": 1.4,
|
|
78
|
+
"font_size": 8.5,
|
|
79
|
+
# The house panel size in inches. Only the *ratio* travels to the SPA (a
|
|
80
|
+
# browser panel is sized in CSS pixels), but both are served so the client
|
|
81
|
+
# never has to hardcode a divisor and a future absolute use has the numbers.
|
|
82
|
+
"fig_w": 3.5,
|
|
83
|
+
"fig_h": 2.45,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@lru_cache(maxsize=1)
|
|
88
|
+
def _plot_style() -> dict:
|
|
89
|
+
"""Read the house plot style out of ``aggregate.style``.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
dict
|
|
94
|
+
Matches :class:`aggregate_api.models.StyleResponse`.
|
|
95
|
+
|
|
96
|
+
Notes
|
|
97
|
+
-----
|
|
98
|
+
This exists so the SPA's interactive charts and the server-rendered
|
|
99
|
+
matplotlib plots are the same colors **by construction** rather than by two
|
|
100
|
+
copies of the same hex list drifting apart. The color cycle is the one
|
|
101
|
+
thing that would be noticed immediately if it did.
|
|
102
|
+
|
|
103
|
+
``rc_params()`` returns a matplotlib rc mapping, where the cycle is an
|
|
104
|
+
``axes.prop_cycle`` ``Cycler`` rather than a plain list, so it is walked
|
|
105
|
+
once here and cached. Every key is read defensively: a style that stops
|
|
106
|
+
setting one falls back rather than 500ing a route the front page calls on
|
|
107
|
+
load.
|
|
108
|
+
|
|
109
|
+
``fig_w`` / ``fig_h`` come from ``aggregate.constants`` rather than from the
|
|
110
|
+
rc mapping, because ``figure.figsize`` is not what the library's own plots
|
|
111
|
+
use: they pass explicit multiples of ``FIG_W`` / ``FIG_H`` per panel
|
|
112
|
+
(``figsize=(2 * FIG_W, FIG_H)`` and so on). The SPA wants the **per-panel**
|
|
113
|
+
aspect, which is exactly that pair.
|
|
114
|
+
"""
|
|
115
|
+
style = dict(_STYLE_FALLBACK)
|
|
116
|
+
try:
|
|
117
|
+
from aggregate.constants import FIG_H, FIG_W
|
|
118
|
+
|
|
119
|
+
style["fig_w"] = float(FIG_W)
|
|
120
|
+
style["fig_h"] = float(FIG_H)
|
|
121
|
+
except Exception: # noqa: BLE001 (fall back to the shipped ratio)
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
from aggregate import style as agg_style
|
|
126
|
+
|
|
127
|
+
rc = agg_style.rc_params()
|
|
128
|
+
except Exception: # noqa: BLE001 (a missing style must not break the page)
|
|
129
|
+
return style
|
|
130
|
+
|
|
131
|
+
cycle = rc.get("axes.prop_cycle")
|
|
132
|
+
if cycle is not None:
|
|
133
|
+
colors = [entry.get("color") for entry in cycle if entry.get("color")]
|
|
134
|
+
if colors:
|
|
135
|
+
style["colors"] = colors
|
|
136
|
+
for key, rc_key in (
|
|
137
|
+
("grid_color", "grid.color"),
|
|
138
|
+
("text_color", "text.color"),
|
|
139
|
+
("line_width", "lines.linewidth"),
|
|
140
|
+
("font_size", "font.size"),
|
|
141
|
+
):
|
|
142
|
+
value = rc.get(rc_key)
|
|
143
|
+
if value is not None:
|
|
144
|
+
style[key] = float(value) if isinstance(value, (int, float)) else str(value)
|
|
145
|
+
return style
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router.get("/meta/style", response_model=models.StyleResponse)
|
|
149
|
+
def plot_style() -> dict:
|
|
150
|
+
"""The house plot style, so the SPA charts match the native plots."""
|
|
151
|
+
return _plot_style()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.get("/meta", response_model=models.MetaResponse)
|
|
155
|
+
async def meta() -> dict:
|
|
156
|
+
"""Echo the live config knobs the SPA needs.
|
|
157
|
+
|
|
158
|
+
``async def`` with the settings read in the body rather than through
|
|
159
|
+
``Depends(get_settings)``, for the reason given on :func:`health`: a sync
|
|
160
|
+
dependency runs in the thread pool and needs one of anyio's 40 tokens, so a
|
|
161
|
+
route carrying one stops answering at exactly the moment the pool is
|
|
162
|
+
exhausted and somebody wants to know what this process is. ``get_settings``
|
|
163
|
+
is an ``lru_cache`` singleton, so calling it directly is a dict lookup and
|
|
164
|
+
the dependency bought nothing here beyond the token.
|
|
165
|
+
|
|
166
|
+
Flask users: ``Depends`` is FastAPI's dependency-injection primitive, the
|
|
167
|
+
closest analogue to ``flask.current_app.config`` but typed and validated. It
|
|
168
|
+
is the right tool nearly everywhere; this route is the exception, and why is
|
|
169
|
+
above.
|
|
170
|
+
"""
|
|
171
|
+
settings = get_settings()
|
|
172
|
+
return {
|
|
173
|
+
"version": _pkg_version("aggregate_api"),
|
|
174
|
+
"aggregate_version": _pkg_version("aggregate"),
|
|
175
|
+
"tables_version": _pkg_version("greater-tables"),
|
|
176
|
+
"log2_cap": settings.log2_cap,
|
|
177
|
+
"log2_default": settings.log2_default,
|
|
178
|
+
"build_timeout_s": settings.build_timeout_s,
|
|
179
|
+
"cache_max": settings.cache_max,
|
|
180
|
+
"plugins": _plugin_manifest(),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _plugin_manifest() -> list[dict]:
|
|
185
|
+
"""The loaded plugins, their versions, their leaves and any load failure.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
list of dict
|
|
190
|
+
Matching :class:`aggregate_api.models.PluginInfo`, in the order
|
|
191
|
+
:func:`aggregate.plugins.loaded_plugins` reports, which is plugin name
|
|
192
|
+
alphabetically. Empty on a stock install and whenever
|
|
193
|
+
``AGGAPI_PLUGINS_ENABLED`` is off.
|
|
194
|
+
|
|
195
|
+
Notes
|
|
196
|
+
-----
|
|
197
|
+
Read per request rather than cached, unlike :func:`_plot_style`. It is a list
|
|
198
|
+
comprehension over a handful of frozen dataclasses, and a cache would add a
|
|
199
|
+
second place for a stale answer to live on the one route whose whole job is
|
|
200
|
+
to say what this process currently is.
|
|
201
|
+
|
|
202
|
+
Only the **last line** of a failure's traceback travels. See
|
|
203
|
+
:class:`aggregate_api.models.PluginInfo` for why.
|
|
204
|
+
"""
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
"name": plugin.name,
|
|
208
|
+
"version": plugin.version,
|
|
209
|
+
"source": plugin.source,
|
|
210
|
+
"leaves": [
|
|
211
|
+
{
|
|
212
|
+
"name": leaf.name,
|
|
213
|
+
"kind": leaf.kind,
|
|
214
|
+
"label": leaf.label,
|
|
215
|
+
"hint": leaf.hint,
|
|
216
|
+
"why": leaf.why,
|
|
217
|
+
}
|
|
218
|
+
for leaf in plugin.leaves
|
|
219
|
+
],
|
|
220
|
+
"error": _one_line(plugin.error),
|
|
221
|
+
}
|
|
222
|
+
for plugin in agg_plugins.loaded_plugins()
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _one_line(error: str | None) -> str | None:
|
|
227
|
+
"""The last non-empty line of a traceback, which is the exception itself."""
|
|
228
|
+
if not error:
|
|
229
|
+
return None
|
|
230
|
+
lines = [line.strip() for line in str(error).strip().splitlines()]
|
|
231
|
+
return next((line for line in reversed(lines) if line), None)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ----------------------------------------------------------------------
|
|
235
|
+
# GET /v1/assets/{name} -- the table-document walker, out of the package
|
|
236
|
+
# ----------------------------------------------------------------------
|
|
237
|
+
# The SPA renders static tables from the IR that `tables.py` emits, using a
|
|
238
|
+
# walker that ships inside `greater_tables` itself. Serving it from the
|
|
239
|
+
# installed package rather than bundling a copy is what makes version skew
|
|
240
|
+
# between the document and its renderer impossible: one install ships both, so
|
|
241
|
+
# they move together or not at all.
|
|
242
|
+
#
|
|
243
|
+
# Deliberately not a StaticFiles mount. Two files, an allow-list, and an
|
|
244
|
+
# explicit media type is less machinery than a mount plus the traversal
|
|
245
|
+
# reasoning a mount invites.
|
|
246
|
+
_ASSETS = {
|
|
247
|
+
"gt-render.esm.js": "text/javascript",
|
|
248
|
+
"gt.css": "text/css",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@lru_cache(maxsize=None)
|
|
253
|
+
def _asset(name: str) -> bytes:
|
|
254
|
+
"""Read one packaged asset. Cached: these are small and never change."""
|
|
255
|
+
return (files("greater_tables") / "assets" / name).read_bytes()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@router.get("/assets/{name}")
|
|
259
|
+
def asset(name: str, request: Request) -> Response:
|
|
260
|
+
"""Serve a ``greater_tables`` front-end asset.
|
|
261
|
+
|
|
262
|
+
Notes
|
|
263
|
+
-----
|
|
264
|
+
Revalidation rather than cache busting. The ETag is the package version, and
|
|
265
|
+
``no-cache`` asks the browser to check it every load, so a `uv sync` that
|
|
266
|
+
moves ``greater_tables`` is picked up on the next reload with no ``?v=``
|
|
267
|
+
for the client to compute and no stale-asset window.
|
|
268
|
+
"""
|
|
269
|
+
media_type = _ASSETS.get(name)
|
|
270
|
+
if media_type is None:
|
|
271
|
+
raise HTTPException(
|
|
272
|
+
status_code=404,
|
|
273
|
+
detail=f"unknown asset {name!r}; expected one of {sorted(_ASSETS)}",
|
|
274
|
+
)
|
|
275
|
+
etag = f'"{_pkg_version("greater-tables")}"'
|
|
276
|
+
if request.headers.get("if-none-match") == etag:
|
|
277
|
+
return Response(status_code=304, headers={"ETag": etag})
|
|
278
|
+
return Response(
|
|
279
|
+
content=_asset(name),
|
|
280
|
+
media_type=media_type,
|
|
281
|
+
headers={"ETag": etag, "Cache-Control": "no-cache"},
|
|
282
|
+
)
|