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,94 @@
1
+ """The recipe base every build in this process resolves against.
2
+
3
+ One underwriter, reached through :func:`get_underwriter`, and every path that
4
+ parses a program or reads a recipe goes through it. Before this module the api
5
+ had two: ``examples.py`` built a private :class:`Underwriter` for the menu when
6
+ ``--library`` was set, while all three build paths used ``aggregate.build``, the
7
+ shipped singleton. That split is invisible under the default library, because
8
+ both are then the same object, and is wrong under a custom one:
9
+
10
+ * an entry is browsable in the Examples menu but any entry referencing a sibling
11
+ by name fails to build, because the base the parser resolves against never
12
+ read the file;
13
+ * the ``.agg`` download lists the session rows of a base the menu did not come
14
+ from.
15
+
16
+ Why a function and not a module-level object: the library is chosen by a setting
17
+ read at process start, and tests flip that setting per case. ``lru_cache`` gives
18
+ the singleton without the import-time evaluation a module-level object would
19
+ force, matching :func:`aggregate_api.config.get_settings`, and
20
+ ``create_app`` clears it for the same reason it clears that one.
21
+
22
+ The base is process-global and mutable: ``build(program)`` registers what it
23
+ parses. Whether that base is shared or per session is a question for the session
24
+ work; this module answers only "which library", and answers it once.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import time
30
+ import warnings
31
+ from functools import lru_cache
32
+ from pathlib import Path
33
+
34
+ from . import status
35
+
36
+
37
+ @lru_cache(maxsize=1)
38
+ def get_underwriter():
39
+ """Return the ``Underwriter`` whose recipe base backs this process.
40
+
41
+ Defaults to ``aggregate.build``, the shipped singleton loaded from
42
+ ``library.agg``. ``AGGAPI_LIBRARY`` overrides it with a custom ``.agg``: a
43
+ private :class:`Underwriter` is pointed at that file via ``databases=`` and
44
+ loaded, so a curated library goes through exactly the same recipe machinery
45
+ as the shipped one.
46
+
47
+ Returns
48
+ -------
49
+ aggregate.underwriter.Underwriter
50
+
51
+ Notes
52
+ -----
53
+ A configured but missing path warns and falls back rather than failing every
54
+ route, which is right for a stale setting on a server. The ``--library``
55
+ flag is deliberately stricter and exits instead: see
56
+ :func:`aggregate_api.__main__.resolve_library` for why the asymmetry is
57
+ intended rather than an oversight.
58
+
59
+ **The load is explicit and timed**, for ``GET /v1/status``. ``load()`` with
60
+ no argument is idempotent (it returns an empty list once the configured
61
+ request has been read), so calling it here costs nothing when the base is
62
+ already loaded and moves nothing when it is not: every caller reaches this
63
+ base through ``fork()``, which loads the parent itself. What it buys is a
64
+ number. The recorded figure is what *this* call paid, so a process reading
65
+ the shipped library reports a near-zero time, correctly: the read happened
66
+ when ``aggregate`` was imported, not here. A custom ``.agg`` reports the
67
+ real cost of reading it, which is the case where the number is worth
68
+ knowing.
69
+ """
70
+ from .config import get_settings
71
+
72
+ started = time.monotonic()
73
+ custom = get_settings().library
74
+ if not custom:
75
+ from aggregate import build
76
+
77
+ uw = build
78
+ else:
79
+ path = Path(custom)
80
+ if not path.is_file():
81
+ warnings.warn(
82
+ f"AGGAPI_LIBRARY={custom!r} not found; using the shipped library",
83
+ stacklevel=2,
84
+ )
85
+ from aggregate import build
86
+
87
+ uw = build
88
+ else:
89
+ from aggregate import Underwriter
90
+
91
+ uw = Underwriter(databases=(str(path),))
92
+ uw.load()
93
+ status.record_library_load((time.monotonic() - started) * 1000)
94
+ return uw
@@ -0,0 +1,96 @@
1
+ """Telling the library's warnings from everyone else's.
2
+
3
+ ``warnings.catch_warnings(record=True)`` plus ``simplefilter('always')`` is the
4
+ only way to capture what ``aggregate`` says on its way to an answer, and it is a
5
+ blunt instrument: the filters are process global and ``'always'`` lifts the
6
+ default suppressions, so the block catches every warning raised anywhere in the
7
+ interpreter for as long as it is open, including ones this service provokes
8
+ about itself.
9
+
10
+ That is not hypothetical. The api's audit log leaked sqlite connections until
11
+ a70, and a garbage collection landing inside a capture block reported
12
+ ``unclosed database in <sqlite3.Connection ...>`` as something the reader's
13
+ program had done. The leak is fixed; this module is the other half, and it is
14
+ the half that keeps the *next* stray warning, from any library in the
15
+ environment, out of a reader's face.
16
+
17
+ Every capture site in this package goes through :func:`library_warnings`, and
18
+ the one site that also needs the logging channel (the build route) uses
19
+ :func:`from_library` directly inside its own richer context manager.
20
+
21
+ The test is **path containment, not the category and not the name.** A library
22
+ ``UserWarning`` is indistinguishable from anyone else's by type, and
23
+ ``aggregate_api`` contains the string ``aggregate``, so a substring test would
24
+ claim this service's own warnings as the library's.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import warnings
30
+ from contextlib import contextmanager
31
+ from pathlib import Path
32
+
33
+ import aggregate as _aggregate_pkg
34
+
35
+ #: Where the ``aggregate`` package lives on disk. Resolved once at import: the
36
+ #: path cannot move while the process runs, and ``Path.resolve`` on every
37
+ #: warning of every build would be work for an answer that never changes.
38
+ _LIBRARY_ROOT = str(Path(_aggregate_pkg.__file__).resolve().parent)
39
+
40
+
41
+ def from_library(filename: str) -> bool:
42
+ """Was this warning raised from inside ``aggregate``?
43
+
44
+ Parameters
45
+ ----------
46
+ filename : str
47
+ ``warnings.WarningMessage.filename``, the source file of the
48
+ ``warn()`` call.
49
+
50
+ Returns
51
+ -------
52
+ bool
53
+ True when the file sits under the installed ``aggregate`` package.
54
+
55
+ Notes
56
+ -----
57
+ Resolved on both sides so a junctioned or symlinked checkout compares
58
+ equal, which this repo's ``.venv`` arrangement makes a live concern rather
59
+ than a theoretical one.
60
+ """
61
+ if not filename:
62
+ return False
63
+ try:
64
+ return Path(filename).resolve().is_relative_to(_LIBRARY_ROOT)
65
+ except (OSError, ValueError): # pragma: no cover -- unresolvable path
66
+ return False
67
+
68
+
69
+ @contextmanager
70
+ def library_warnings():
71
+ """Capture ``warnings.warn`` from ``aggregate``, and only from it.
72
+
73
+ Yields
74
+ ------
75
+ list of str
76
+ Empty while the block runs and filled on exit, so callers extend their
77
+ own list *after* the ``with``, not inside it.
78
+
79
+ Examples
80
+ --------
81
+ >>> with library_warnings() as caught: # doctest: +SKIP
82
+ ... panel = obj.evaluate(premium)
83
+ >>> warns.extend(caught) # doctest: +SKIP
84
+
85
+ Notes
86
+ -----
87
+ Filled on exit rather than as each warning arrives because
88
+ ``catch_warnings(record=True)`` hands back a list it appends to itself, and
89
+ the filtering has to happen once that list is complete.
90
+ """
91
+ kept: list[str] = []
92
+ with warnings.catch_warnings(record=True) as caught:
93
+ warnings.simplefilter("always")
94
+ yield kept
95
+ kept.extend(str(w.message) for w in caught
96
+ if from_library(getattr(w, "filename", "")))