openbb-tensorust 0.1.0__tar.gz
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.
- openbb_tensorust-0.1.0/.gitignore +9 -0
- openbb_tensorust-0.1.0/CHANGELOG.md +14 -0
- openbb_tensorust-0.1.0/LICENSE +21 -0
- openbb_tensorust-0.1.0/PKG-INFO +65 -0
- openbb_tensorust-0.1.0/docs/index.md +91 -0
- openbb_tensorust-0.1.0/examples/contractor_and_compliance.py +28 -0
- openbb_tensorust-0.1.0/examples/private_credit_screen.py +21 -0
- openbb_tensorust-0.1.0/openbb_tensorust/__init__.py +52 -0
- openbb_tensorust-0.1.0/openbb_tensorust/client.py +110 -0
- openbb_tensorust-0.1.0/openbb_tensorust/models/__init__.py +1 -0
- openbb_tensorust-0.1.0/openbb_tensorust/models/contractor.py +213 -0
- openbb_tensorust-0.1.0/openbb_tensorust/models/credit.py +261 -0
- openbb_tensorust-0.1.0/openbb_tensorust/models/exposure.py +107 -0
- openbb_tensorust-0.1.0/openbb_tensorust/models/idr.py +128 -0
- openbb_tensorust-0.1.0/openbb_tensorust/router.py +115 -0
- openbb_tensorust-0.1.0/pyproject.toml +96 -0
- openbb_tensorust-0.1.0/tests/integration_tests/__init__.py +0 -0
- openbb_tensorust-0.1.0/tests/integration_tests/test_live.py +60 -0
- openbb_tensorust-0.1.0/tests/unit_tests/__init__.py +0 -0
- openbb_tensorust-0.1.0/tests/unit_tests/conftest.py +59 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/contractor_credit_preview.json +104 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/contractor_credit_watch_preview.json +113 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/credit_disagreements_full.json +37 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/credit_disagreements_preview.json +39 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/credit_q3_cuts_full.json +26 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/credit_q3_cuts_preview.json +36 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/exposure_institution_preview.json +136 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/idr_offer_preview.json +32 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/lender_marks_full.json +50 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/lender_marks_missing.json +6 -0
- openbb_tensorust-0.1.0/tests/unit_tests/fixtures/lender_marks_preview.json +30 -0
- openbb_tensorust-0.1.0/tests/unit_tests/test_extension.py +57 -0
- openbb_tensorust-0.1.0/tests/unit_tests/test_fetchers.py +198 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-15)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- OpenBB provider `tensorust` and router `obb.tensorust` with seven commands:
|
|
8
|
+
`bdc_loan_marks`, `credit_disagreements`, `credit_q3_cuts`, `contractor_credit`,
|
|
9
|
+
`contractor_watch`, `exposure_institution` and `idr_offer`.
|
|
10
|
+
- Free previews without a key. `tensorust_api_key` (or `TENSORUST_API_KEY`) returns full records.
|
|
11
|
+
- Summary fields (gap, totals, rule status counts, arbitrator table) are in
|
|
12
|
+
`OBBject.extra["results_metadata"]`.
|
|
13
|
+
- Works with the Python interface, the OpenBB REST API (`openbb-api`) and OpenBB's MCP server.
|
|
14
|
+
- Tested against openbb-core 1.6.9 and 2.0.1.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Busel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openbb-tensorust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenBB extension for Tensorust data: BDC private credit loan marks, federal contractor credit, banned-vendor exposure in federal awards and No Surprises Act IDR offer bands.
|
|
5
|
+
Project-URL: Homepage, https://tensorust.com
|
|
6
|
+
Project-URL: Source, https://github.com/Mattbusel/openbb-tensorust
|
|
7
|
+
Project-URL: Documentation, https://github.com/Mattbusel/openbb-tensorust/blob/main/docs/index.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/Mattbusel/openbb-tensorust/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Workspace backend, https://shop.tensorust.com/api/openbb
|
|
10
|
+
Author: Matthew Busel
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: bdc,federal-contracts,finance,loan-marks,no-surprises-act,openbb,private-credit,sanctions
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: openbb-core<3,>=1.6
|
|
25
|
+
Requires-Dist: requests>=2.31
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# openbb-tensorust
|
|
29
|
+
|
|
30
|
+
An [OpenBB Platform](https://github.com/OpenBB-finance/OpenBB) extension for
|
|
31
|
+
[Tensorust](https://tensorust.com) data, built from US public records.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install openbb openbb-tensorust
|
|
35
|
+
openbb-build # once, so obb picks up the new commands
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from openbb import obb
|
|
40
|
+
|
|
41
|
+
obb.tensorust.credit_disagreements().to_df() # loans BDC lenders mark 10+ points apart
|
|
42
|
+
obb.tensorust.bdc_loan_marks(company="Pluralsight").to_df()
|
|
43
|
+
obb.tensorust.credit_q3_cuts().to_df() # Q3 2026 markdowns as BDCs file
|
|
44
|
+
obb.tensorust.contractor_credit(contractor="Peraton")
|
|
45
|
+
obb.tensorust.contractor_watch(sort="q3").to_df()
|
|
46
|
+
obb.tensorust.exposure_institution(institution="Harvard").to_df()
|
|
47
|
+
obb.tensorust.idr_offer(code="99284", payer="uhc.com", state="TX")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Command | Answers |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `bdc_loan_marks` | What each BDC fund marks one company's first-lien loan at (SEC schedules of investments) |
|
|
53
|
+
| `credit_disagreements` | Loans two or more BDC managers mark 10+ points apart |
|
|
54
|
+
| `credit_q3_cuts` | Fund-level cuts of 5+ points from Jun 30 to Sep 30 2026, and who has not filed yet |
|
|
55
|
+
| `contractor_credit` | A federal contractor's obligations, lender marks, headcount trend and stress signals |
|
|
56
|
+
| `contractor_watch` | Federal contractors whose lenders are marking them down |
|
|
57
|
+
| `exposure_institution` | Federal awards whose papers name vendors on the 1260H, Section 889, FCC Covered, UFLPA or BIOSECURE lists |
|
|
58
|
+
| `idr_offer` | No Surprises Act federal IDR: provider win rate and the winning offer band by code, payer and state |
|
|
59
|
+
|
|
60
|
+
No key is needed: every command answers a free preview (top 10 rows, or a summary).
|
|
61
|
+
Set `obb.user.credentials.tensorust_api_key = "tdk_..."` (or `TENSORUST_API_KEY`) for full
|
|
62
|
+
records. The same data loads in OpenBB Workspace as a custom backend:
|
|
63
|
+
`https://shop.tensorust.com/api/openbb`.
|
|
64
|
+
|
|
65
|
+
Docs: https://github.com/Mattbusel/openbb-tensorust/blob/main/docs/index.md
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# openbb-tensorust
|
|
2
|
+
|
|
3
|
+
OpenBB extension for [Tensorust](https://tensorust.com) data. Seven commands under
|
|
4
|
+
`obb.tensorust`, served by the `tensorust` provider. Every command answers a free preview
|
|
5
|
+
without a key; a Tensorust key returns full records.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install openbb openbb-tensorust
|
|
11
|
+
openbb-build # regenerates the obb package so the new commands appear
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Optional key (full records):
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from openbb import obb
|
|
18
|
+
|
|
19
|
+
obb.user.credentials.tensorust_api_key = "tdk_..."
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
or `export TENSORUST_API_KEY=tdk_...`. Keys: https://shop.tensorust.com/offers/enterprise-api
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
| Command | Parameters | Rows | Free preview | With a key |
|
|
27
|
+
|---|---|---|---|---|
|
|
28
|
+
| `bdc_loan_marks` | `company` | one per BDC fund holding the loan | up to 3 funds, mark only | every fund: par, prior-quarter mark, Sep 30 mark, filing link |
|
|
29
|
+
| `credit_disagreements` | none | one per company marked 10+ points apart | top 10 | all |
|
|
30
|
+
| `credit_q3_cuts` | none | one per fund cut of 5+ points, Jun 30 to Sep 30 2026 | top 10 | all, with each holder not filed yet (`pending_holders`) |
|
|
31
|
+
| `contractor_credit` | `contractor` (name, UEI or EIN) | one | profile | adds every lender line, recipients, mark history |
|
|
32
|
+
| `contractor_watch` | `sort`, `private_only`, `agency`, `state`, `signal` | one per stressed contractor | top 10 | all |
|
|
33
|
+
| `exposure_institution` | `institution` (name or UEI) | one per award | 5 largest awards | every award with its evidence sentence and paper link |
|
|
34
|
+
| `idr_offer` | `code`, `payer`, `state`, `entity`, `qpa` | one cell | win rate and offer band | quartiles, every arbitrator's loss rate and 2026 fee, expected value at your QPA (cells under 50 determinations are free in full) |
|
|
35
|
+
|
|
36
|
+
Marks are fractions of par (`0.95` = 95% of par). Summary fields for each answer (the
|
|
37
|
+
lowest and highest mark, totals by list, the arbitrator table, how to get the full record)
|
|
38
|
+
are in `result.extra["results_metadata"]`.
|
|
39
|
+
|
|
40
|
+
Not found is an `EmptyDataError` naming the closest matches (HTTP 204 from `openbb-api`).
|
|
41
|
+
|
|
42
|
+
## Sources
|
|
43
|
+
|
|
44
|
+
- **Private credit marks:** SEC Business Development Company data sets (XBRL schedules of
|
|
45
|
+
investments), first-lien term loans, fully funded, cost 85-110% of par; Sep 30 2026
|
|
46
|
+
marks from 10-Qs as filed.
|
|
47
|
+
- **Contractor Credit:** the marks above joined to USAspending.gov recipients, SAM parent
|
|
48
|
+
UEI, DOL Form 5500 headcount, FCC and FAA records.
|
|
49
|
+
- **Exposure Ledger:** Federal Register (DoD 1260H), FAR 52.204-25, FCC Covered List, DHS
|
|
50
|
+
UFLPA Entity List, BIOSECURE; evidence from Europe PMC, OpenAlex, Crossref, open-access
|
|
51
|
+
PDFs, ENA and dbGaP; dollars from USAspending and NIH RePORTER.
|
|
52
|
+
- **IDR:** CMS Federal IDR Public Use File 2025 Q4, computed by Tensorust LLC (altered and
|
|
53
|
+
aggregated; not CMS data as published).
|
|
54
|
+
|
|
55
|
+
## REST API and MCP
|
|
56
|
+
|
|
57
|
+
`openbb-api` serves the commands at `/api/v1/tensorust/<command>?provider=tensorust`,
|
|
58
|
+
and OpenBB's MCP server exposes them as tools like any other OpenBB command.
|
|
59
|
+
|
|
60
|
+
## OpenBB Workspace
|
|
61
|
+
|
|
62
|
+
The same data is available in OpenBB Workspace without installing anything: add a custom
|
|
63
|
+
backend with the URL
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
https://shop.tensorust.com/api/openbb
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It serves `widgets.json` (9 widgets: loan marks table and summary, disagreements, Q3 cuts,
|
|
70
|
+
contractor profile and watchlist, exposure table and summary, IDR offer band) and
|
|
71
|
+
`apps.json` (two apps: *Tensorust Private Credit* and *Tensorust Compliance and IDR*).
|
|
72
|
+
Clicking a company in the disagreements or Q3 cuts table loads its marks; clicking a
|
|
73
|
+
contractor in the watchlist loads its profile. For full records add the header
|
|
74
|
+
`Authorization: Bearer tdk_...` to the backend connection.
|
|
75
|
+
|
|
76
|
+
## How it works
|
|
77
|
+
|
|
78
|
+
Each command makes one JSON-RPC `tools/call` to the public Tensorust MCP server
|
|
79
|
+
(`https://shop.tensorust.com/api/mcp`, override with `TENSORUST_MCP_URL`). With a key it
|
|
80
|
+
sends `full=true` and the bearer token. No other network calls, no telemetry.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install -e . pytest pytest-asyncio httpx ruff mypy
|
|
86
|
+
pytest tests/unit_tests # no network
|
|
87
|
+
pytest tests/integration_tests # keyless calls to the public endpoint
|
|
88
|
+
ruff format --check . && ruff check . && mypy openbb_tensorust
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
MIT licensed. Maintainer: Matthew Busel (mattbusel@tensorust.com).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Federal contractors on marked-down loans, an institution's banned-vendor exposure, and an IDR offer band.
|
|
2
|
+
|
|
3
|
+
pip install openbb openbb-tensorust && openbb-build
|
|
4
|
+
python examples/contractor_and_compliance.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from openbb import obb
|
|
8
|
+
|
|
9
|
+
watch = obb.tensorust.contractor_watch(sort="q3", private_only=True).to_df()
|
|
10
|
+
print(
|
|
11
|
+
watch[["operating_company", "state", "federal_exposure", "latest_mark", "q3_change_points"]]
|
|
12
|
+
.head()
|
|
13
|
+
.to_string(index=False)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
one = obb.tensorust.contractor_credit(contractor=watch["uei"].iloc[0])
|
|
17
|
+
print("\n" + (one.results[0].read or ""))
|
|
18
|
+
|
|
19
|
+
awards = obb.tensorust.exposure_institution(institution="Johns Hopkins")
|
|
20
|
+
meta = awards.extra["results_metadata"]
|
|
21
|
+
print(
|
|
22
|
+
f"\n{meta['institution']}: {meta['awards']} awards, ${meta['fy25']:,.0f} FY25, {meta['in_force_awards']} bind now"
|
|
23
|
+
)
|
|
24
|
+
print(awards.to_df()[["award", "agency", "fy25_obligations", "lists", "attribution"]].to_string(index=False))
|
|
25
|
+
|
|
26
|
+
idr = obb.tensorust.idr_offer(code="99284", payer="uhc.com", state="TX").results[0]
|
|
27
|
+
band = f"{idr.offer_band_low_multiple}x to {idr.offer_band_high_multiple}x QPA"
|
|
28
|
+
print(f"\nCPT 99284 / UHC / TX: providers win {idr.provider_win_rate_pct}%, offer {band}")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Screen BDC loans where lenders disagree, then pull each fund's mark for the widest gaps.
|
|
2
|
+
|
|
3
|
+
pip install openbb openbb-tensorust && openbb-build
|
|
4
|
+
python examples/private_credit_screen.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from openbb import obb
|
|
8
|
+
|
|
9
|
+
gaps = obb.tensorust.credit_disagreements()
|
|
10
|
+
print(gaps.extra["results_metadata"].get("access", "full records"))
|
|
11
|
+
df = gaps.to_df()
|
|
12
|
+
print(df[["company", "gap_points", "lowest_manager", "lowest_mark", "highest_manager", "highest_mark"]].head())
|
|
13
|
+
|
|
14
|
+
for company in df["company"].head(3):
|
|
15
|
+
marks = obb.tensorust.bdc_loan_marks(company=company)
|
|
16
|
+
print(f"\n{company}: {marks.extra['results_metadata'].get('read', '')}")
|
|
17
|
+
print(marks.to_df()[["fund", "manager", "mark"]].to_string(index=False))
|
|
18
|
+
|
|
19
|
+
cuts = obb.tensorust.credit_q3_cuts().to_df()
|
|
20
|
+
print("\nBiggest Q3 2026 markdowns so far:")
|
|
21
|
+
print(cuts[["company", "fund", "jun30_mark", "sep30_mark", "change_points"]].head().to_string(index=False))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""OpenBB provider and router extension for Tensorust data.
|
|
2
|
+
|
|
3
|
+
Private credit loan marks from SEC BDC filings, federal contractor credit,
|
|
4
|
+
banned-vendor exposure in federal research awards, and No Surprises Act IDR
|
|
5
|
+
offer bands. Free previews without a key; set ``tensorust_api_key`` (or the
|
|
6
|
+
``TENSORUST_API_KEY`` environment variable) for full records.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from openbb_core.provider.abstract.provider import Provider
|
|
10
|
+
|
|
11
|
+
from openbb_tensorust.models.contractor import (
|
|
12
|
+
TensorustContractorCreditFetcher,
|
|
13
|
+
TensorustContractorWatchFetcher,
|
|
14
|
+
)
|
|
15
|
+
from openbb_tensorust.models.credit import (
|
|
16
|
+
TensorustBdcLoanMarksFetcher,
|
|
17
|
+
TensorustCreditDisagreementsFetcher,
|
|
18
|
+
TensorustCreditQ3CutsFetcher,
|
|
19
|
+
)
|
|
20
|
+
from openbb_tensorust.models.exposure import TensorustExposureInstitutionFetcher
|
|
21
|
+
from openbb_tensorust.models.idr import TensorustIdrOfferFetcher
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
tensorust_provider = Provider(
|
|
26
|
+
name="tensorust",
|
|
27
|
+
website="https://tensorust.com",
|
|
28
|
+
description=(
|
|
29
|
+
"Tensorust builds entity-level datasets from US public records: what BDC (private credit) "
|
|
30
|
+
"lenders mark each first-lien loan at, which federal contractors sit on marked-down loans, "
|
|
31
|
+
"which federal research awards pay for work by vendors on banned-vendor lists, and the "
|
|
32
|
+
"No Surprises Act IDR offer band by code, payer and state. Every command answers a free "
|
|
33
|
+
"preview without a key."
|
|
34
|
+
),
|
|
35
|
+
credentials=["api_key"],
|
|
36
|
+
fetcher_dict={
|
|
37
|
+
"TensorustBdcLoanMarks": TensorustBdcLoanMarksFetcher,
|
|
38
|
+
"TensorustCreditDisagreements": TensorustCreditDisagreementsFetcher,
|
|
39
|
+
"TensorustCreditQ3Cuts": TensorustCreditQ3CutsFetcher,
|
|
40
|
+
"TensorustContractorCredit": TensorustContractorCreditFetcher,
|
|
41
|
+
"TensorustContractorWatch": TensorustContractorWatchFetcher,
|
|
42
|
+
"TensorustExposureInstitution": TensorustExposureInstitutionFetcher,
|
|
43
|
+
"TensorustIdrOffer": TensorustIdrOfferFetcher,
|
|
44
|
+
},
|
|
45
|
+
repr_name="Tensorust",
|
|
46
|
+
instructions=(
|
|
47
|
+
"No key is needed for previews. For full records set the credential tensorust_api_key "
|
|
48
|
+
"to a Tensorust enterprise key (tdk_...): https://shop.tensorust.com/offers/enterprise-api"
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = ["__version__", "tensorust_provider"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Tensorust MCP client: one JSON-RPC ``tools/call`` per OpenBB command.
|
|
2
|
+
|
|
3
|
+
Every fetcher in this package calls the public Tensorust MCP server
|
|
4
|
+
(https://shop.tensorust.com/api/mcp). Without a key it answers free previews.
|
|
5
|
+
With a Tensorust key (``tensorust_api_key`` in OpenBB credentials, or the
|
|
6
|
+
``TENSORUST_API_KEY`` environment variable) the call asks for ``full=true``
|
|
7
|
+
and the server returns full records.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
from openbb_core.app.model.abstract.error import OpenBBError
|
|
18
|
+
from openbb_core.provider.utils.errors import EmptyDataError
|
|
19
|
+
|
|
20
|
+
MCP_URL = "https://shop.tensorust.com/api/mcp"
|
|
21
|
+
KEY_URL = "https://shop.tensorust.com/offers/enterprise-api"
|
|
22
|
+
USER_AGENT = "openbb-tensorust"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def mcp_url() -> str:
|
|
26
|
+
"""Endpoint, overridable with TENSORUST_MCP_URL (tests, staging)."""
|
|
27
|
+
return os.environ.get("TENSORUST_MCP_URL", MCP_URL)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def api_key(credentials: dict[str, str] | None) -> str | None:
|
|
31
|
+
"""The Tensorust key from OpenBB credentials, else from the environment."""
|
|
32
|
+
key = (credentials or {}).get("tensorust_api_key") or os.environ.get("TENSORUST_API_KEY")
|
|
33
|
+
return key.strip() if key and key.strip() else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def call_tool(
|
|
37
|
+
name: str,
|
|
38
|
+
arguments: dict[str, Any],
|
|
39
|
+
credentials: dict[str, str] | None = None,
|
|
40
|
+
timeout: float = 60.0,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""Call one Tensorust MCP tool and return its JSON answer as a dict.
|
|
43
|
+
|
|
44
|
+
Adds ``full=true`` and the bearer key when a key is configured.
|
|
45
|
+
Raises OpenBBError on transport or tool errors.
|
|
46
|
+
"""
|
|
47
|
+
key = api_key(credentials)
|
|
48
|
+
args = {k: v for k, v in arguments.items() if v is not None and v != ""}
|
|
49
|
+
headers = {
|
|
50
|
+
"content-type": "application/json",
|
|
51
|
+
"accept": "application/json",
|
|
52
|
+
"user-agent": USER_AGENT,
|
|
53
|
+
}
|
|
54
|
+
if key:
|
|
55
|
+
headers["authorization"] = f"Bearer {key}"
|
|
56
|
+
args["full"] = True
|
|
57
|
+
payload = {
|
|
58
|
+
"jsonrpc": "2.0",
|
|
59
|
+
"id": 1,
|
|
60
|
+
"method": "tools/call",
|
|
61
|
+
"params": {"name": name, "arguments": args},
|
|
62
|
+
}
|
|
63
|
+
try:
|
|
64
|
+
resp = requests.post(mcp_url(), json=payload, headers=headers, timeout=timeout)
|
|
65
|
+
except requests.RequestException as exc: # pragma: no cover - network
|
|
66
|
+
raise OpenBBError(f"Tensorust request failed: {exc}") from exc
|
|
67
|
+
if resp.status_code != 200:
|
|
68
|
+
raise OpenBBError(f"Tensorust answered HTTP {resp.status_code}: {resp.text[:300]}")
|
|
69
|
+
body = resp.json()
|
|
70
|
+
if "error" in body:
|
|
71
|
+
raise OpenBBError(f"Tensorust error: {body['error'].get('message', body['error'])}")
|
|
72
|
+
result = body.get("result") or {}
|
|
73
|
+
text = "".join(c.get("text", "") for c in result.get("content", []) if c.get("type") == "text")
|
|
74
|
+
if result.get("isError"):
|
|
75
|
+
raise EmptyDataError(text or f"Tensorust tool {name} returned an error.")
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(text)
|
|
78
|
+
except json.JSONDecodeError as exc:
|
|
79
|
+
raise OpenBBError(f"Tensorust tool {name} returned non-JSON text: {text[:200]}") from exc
|
|
80
|
+
if not isinstance(data, dict):
|
|
81
|
+
raise OpenBBError(f"Tensorust tool {name} returned {type(data).__name__}, not an object.")
|
|
82
|
+
return data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def not_found(data: dict[str, Any], what: str) -> EmptyDataError:
|
|
86
|
+
"""EmptyDataError naming the closest matches the server suggested."""
|
|
87
|
+
similar = data.get("similar") or []
|
|
88
|
+
names = [s.get("name") if isinstance(s, dict) else str(s) for s in similar if s]
|
|
89
|
+
hint = f" Closest names: {', '.join(n for n in names if n)}." if names else ""
|
|
90
|
+
note = f" {data['note']}" if data.get("note") else ""
|
|
91
|
+
return EmptyDataError(f"No {what} matches '{data.get('query', '')}'.{note}{hint}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def access_note(data: dict[str, Any]) -> str | None:
|
|
95
|
+
"""How to get the full answer, when this one is a preview (None when it is already full)."""
|
|
96
|
+
if data.get("access") in ("full", "key", "free"):
|
|
97
|
+
return None
|
|
98
|
+
scope = ""
|
|
99
|
+
for k in ("access", "full_data", "full"):
|
|
100
|
+
v = data.get(k)
|
|
101
|
+
if isinstance(v, str) and v.lower().startswith(("top ", "first ")):
|
|
102
|
+
scope = f" ({v.split(';')[0].strip()})"
|
|
103
|
+
break
|
|
104
|
+
is_preview = data.get("access") is not None or data.get("preview") is True or "full_data" in data
|
|
105
|
+
if not is_preview:
|
|
106
|
+
return None
|
|
107
|
+
return (
|
|
108
|
+
f"Free preview{scope}. For full records set obb.user.credentials.tensorust_api_key "
|
|
109
|
+
f"(or TENSORUST_API_KEY) to a Tensorust key: {KEY_URL}"
|
|
110
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tensorust provider models."""
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Contractor Credit: federal contractors financed by BDC lenders, and how those lenders mark them.
|
|
2
|
+
|
|
3
|
+
Sources: SEC BDC schedules of investments (Q3 2026 from 10-Qs as filed),
|
|
4
|
+
USAspending.gov, SAM parent UEI, DOL Form 5500, FCC, FAA and county records.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
|
12
|
+
from openbb_core.provider.abstract.data import Data
|
|
13
|
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
|
14
|
+
from openbb_core.provider.abstract.query_params import QueryParams
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
from openbb_tensorust.client import access_note, call_tool, not_found
|
|
18
|
+
|
|
19
|
+
MARK = "Fraction of par (0.95 = 95% of par)."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---- contractor_credit ----------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TensorustContractorCreditQueryParams(QueryParams):
|
|
26
|
+
"""Contractor credit query."""
|
|
27
|
+
|
|
28
|
+
contractor: str = Field(description="Federal contractor name, 12-character UEI or 9-digit EIN.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TensorustContractorCreditData(Data):
|
|
32
|
+
"""One federal contractor's credit picture."""
|
|
33
|
+
|
|
34
|
+
operating_company: str = Field(description="Federal award recipient.")
|
|
35
|
+
uei: str | None = Field(default=None, description="SAM UEI of the recipient.")
|
|
36
|
+
state: str | None = Field(default=None, description="Recipient state.")
|
|
37
|
+
borrower: str | None = Field(default=None, description="Borrower name in BDC filings.")
|
|
38
|
+
sam_parent: str | None = Field(default=None, description="Parent reported to SAM.")
|
|
39
|
+
federal_obligations_fy2025_to_date: float | None = Field(default=None, description="USD.")
|
|
40
|
+
dod_fy2024_2026: float | None = Field(default=None, description="DoD obligations FY2024-26, USD.")
|
|
41
|
+
top_agency: str | None = Field(default=None, description="Agency with the most obligations.")
|
|
42
|
+
marks_as_of: str | None = Field(default=None, description="Quarter end of the BDC marks.")
|
|
43
|
+
lowest_mark: float | None = Field(default=None, description="Lowest Jun 30 BDC mark. " + MARK)
|
|
44
|
+
highest_mark: float | None = Field(default=None, description="Highest Jun 30 BDC mark. " + MARK)
|
|
45
|
+
sep30_lowest_mark: float | None = Field(default=None, description="Lowest Sep 30 2026 mark filed so far. " + MARK)
|
|
46
|
+
all_debt_mark: float | None = Field(
|
|
47
|
+
default=None, description="Par-weighted mark of every BDC debt line, latest quarter."
|
|
48
|
+
)
|
|
49
|
+
bdc_par: float | None = Field(default=None, description="BDC first-lien par, USD.")
|
|
50
|
+
headcount_change_pct: float | None = Field(default=None, description="Form 5500 participants, 2024 to 2025, %.")
|
|
51
|
+
signals: list[str] = Field(default_factory=list, description="Stress signals.")
|
|
52
|
+
market_priced: str | None = Field(default=None, description="Set when the credit is already market priced.")
|
|
53
|
+
read: str | None = Field(default=None, description="One-paragraph summary.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TensorustContractorCreditFetcher(
|
|
57
|
+
Fetcher[TensorustContractorCreditQueryParams, list[TensorustContractorCreditData]]
|
|
58
|
+
):
|
|
59
|
+
"""contractor_credit: obligations, lender marks, headcount and signals for one contractor."""
|
|
60
|
+
|
|
61
|
+
require_credentials = False
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def transform_query(params: dict[str, Any]) -> TensorustContractorCreditQueryParams:
|
|
65
|
+
return TensorustContractorCreditQueryParams(**params)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def extract_data(
|
|
69
|
+
query: TensorustContractorCreditQueryParams,
|
|
70
|
+
credentials: dict[str, str] | None,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
return call_tool("contractor_credit", {"name": query.contractor}, credentials)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def transform_data(
|
|
77
|
+
query: TensorustContractorCreditQueryParams, data: dict[str, Any], **kwargs: Any
|
|
78
|
+
) -> AnnotatedResult[list[TensorustContractorCreditData]]:
|
|
79
|
+
if not data.get("found"):
|
|
80
|
+
raise not_found(data, "BDC-financed federal contractor")
|
|
81
|
+
op = data.get("operating_company") or {}
|
|
82
|
+
fed = data.get("federal") or {}
|
|
83
|
+
m = data.get("marks") or {}
|
|
84
|
+
hc = data.get("headcount") or {}
|
|
85
|
+
mp = data.get("market_priced") or {}
|
|
86
|
+
all_debt = m.get("all_debt_lines_latest") or {}
|
|
87
|
+
row = TensorustContractorCreditData(
|
|
88
|
+
operating_company=op.get("name") or query.contractor,
|
|
89
|
+
uei=op.get("uei"),
|
|
90
|
+
state=op.get("state"),
|
|
91
|
+
borrower=data.get("borrower"),
|
|
92
|
+
sam_parent=op.get("sam_parent_reported"),
|
|
93
|
+
federal_obligations_fy2025_to_date=fed.get("obligations_fy2025_to_date"),
|
|
94
|
+
dod_fy2024_2026=fed.get("dod_fy2024_2026"),
|
|
95
|
+
top_agency=fed.get("top_agency"),
|
|
96
|
+
marks_as_of=m.get("as_of"),
|
|
97
|
+
lowest_mark=m.get("lowest"),
|
|
98
|
+
highest_mark=m.get("highest"),
|
|
99
|
+
sep30_lowest_mark=m.get("q3_lowest"),
|
|
100
|
+
all_debt_mark=all_debt.get("mark"),
|
|
101
|
+
bdc_par=m.get("bdc_par"),
|
|
102
|
+
headcount_change_pct=hc.get("change_pct"),
|
|
103
|
+
signals=data.get("signals") or [],
|
|
104
|
+
market_priced=mp.get("kind"),
|
|
105
|
+
read=data.get("read"),
|
|
106
|
+
)
|
|
107
|
+
meta: dict[str, Any] = {
|
|
108
|
+
k: data[k]
|
|
109
|
+
for k in (
|
|
110
|
+
"federal",
|
|
111
|
+
"marks",
|
|
112
|
+
"headcount",
|
|
113
|
+
"footprint",
|
|
114
|
+
"subcontracts_fy2025_to_date",
|
|
115
|
+
"market_priced",
|
|
116
|
+
"agencies",
|
|
117
|
+
"sample_agencies",
|
|
118
|
+
"lenders",
|
|
119
|
+
"q3_pending_holders",
|
|
120
|
+
"recipients",
|
|
121
|
+
"mark_history",
|
|
122
|
+
"source",
|
|
123
|
+
)
|
|
124
|
+
if data.get(k) is not None
|
|
125
|
+
}
|
|
126
|
+
note = access_note(data)
|
|
127
|
+
if note:
|
|
128
|
+
meta["access"] = note
|
|
129
|
+
return AnnotatedResult(result=[row], metadata=meta)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---- contractor_watch -----------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TensorustContractorWatchQueryParams(QueryParams):
|
|
136
|
+
"""Stressed federal contractors query."""
|
|
137
|
+
|
|
138
|
+
sort: Literal["federal", "mark", "q3"] = Field(
|
|
139
|
+
default="federal",
|
|
140
|
+
description="federal: largest federal exposure first; mark: lowest mark first; q3: biggest Sep 30 cut first.",
|
|
141
|
+
)
|
|
142
|
+
private_only: bool = Field(default=False, description="Drop names with listed equity or a rated syndicated loan.")
|
|
143
|
+
agency: str | None = Field(default=None, description="Only contractors paid by this agency (substring).")
|
|
144
|
+
state: str | None = Field(default=None, description="Two-letter recipient state.")
|
|
145
|
+
signal: str | None = Field(
|
|
146
|
+
default=None,
|
|
147
|
+
description="One signal instead of the stress default, e.g. marked_below_80, lenders_10pt_apart, cut_5pt_q3.",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TensorustContractorWatchData(Data):
|
|
152
|
+
"""A federal contractor whose BDC lenders are marking its loan down."""
|
|
153
|
+
|
|
154
|
+
operating_company: str = Field(description="Federal award recipient.")
|
|
155
|
+
borrower: str | None = Field(default=None, description="Borrower name in BDC filings.")
|
|
156
|
+
uei: str | None = Field(default=None, description="SAM UEI.")
|
|
157
|
+
state: str | None = Field(default=None, description="Recipient state.")
|
|
158
|
+
federal_exposure: float | None = Field(
|
|
159
|
+
default=None, description="Larger of FY2025-to-date and DoD FY2024-26 obligations, USD."
|
|
160
|
+
)
|
|
161
|
+
top_agency: str | None = Field(default=None, description="Agency with the most obligations.")
|
|
162
|
+
latest_mark: float | None = Field(default=None, description="Latest BDC mark. " + MARK)
|
|
163
|
+
q3_change_points: float | None = Field(default=None, description="Worst Jun 30 to Sep 30 move, points.")
|
|
164
|
+
managers: int | None = Field(default=None, description="BDC managers on the loan.")
|
|
165
|
+
bdc_par: float | None = Field(default=None, description="BDC first-lien par, USD.")
|
|
166
|
+
headcount_change_pct: float | None = Field(default=None, description="Form 5500 participants, 2024 to 2025, %.")
|
|
167
|
+
signals: list[str] = Field(default_factory=list, description="Stress signals.")
|
|
168
|
+
market_priced: str | None = Field(default=None, description="public or bsl when already market priced.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TensorustContractorWatchFetcher(Fetcher[TensorustContractorWatchQueryParams, list[TensorustContractorWatchData]]):
|
|
172
|
+
"""contractor_credit_watch. Free: top 10; with a key: every name."""
|
|
173
|
+
|
|
174
|
+
require_credentials = False
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def transform_query(params: dict[str, Any]) -> TensorustContractorWatchQueryParams:
|
|
178
|
+
return TensorustContractorWatchQueryParams(**params)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def extract_data(
|
|
182
|
+
query: TensorustContractorWatchQueryParams,
|
|
183
|
+
credentials: dict[str, str] | None,
|
|
184
|
+
**kwargs: Any,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
args: dict[str, Any] = {
|
|
187
|
+
"sort": query.sort,
|
|
188
|
+
"agency": query.agency,
|
|
189
|
+
"state": query.state,
|
|
190
|
+
"signal": query.signal,
|
|
191
|
+
}
|
|
192
|
+
if query.private_only:
|
|
193
|
+
args["private_only"] = True
|
|
194
|
+
return call_tool("contractor_credit_watch", args, credentials)
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def transform_data(
|
|
198
|
+
query: TensorustContractorWatchQueryParams, data: dict[str, Any], **kwargs: Any
|
|
199
|
+
) -> AnnotatedResult[list[TensorustContractorWatchData]]:
|
|
200
|
+
fields = set(TensorustContractorWatchData.model_fields)
|
|
201
|
+
rows = []
|
|
202
|
+
for x in data.get("items") or []:
|
|
203
|
+
known = {k: v for k, v in x.items() if k in fields}
|
|
204
|
+
known["signals"] = x.get("signals") or []
|
|
205
|
+
extra = {k: v for k, v in x.items() if k not in fields and k != "key"}
|
|
206
|
+
rows.append(TensorustContractorWatchData(**known, **extra))
|
|
207
|
+
meta: dict[str, Any] = {
|
|
208
|
+
k: data[k] for k in ("marks_as_of", "q3_filers", "q3_last_filed", "count", "federal_exposure") if k in data
|
|
209
|
+
}
|
|
210
|
+
note = access_note(data)
|
|
211
|
+
if note:
|
|
212
|
+
meta["access"] = note
|
|
213
|
+
return AnnotatedResult(result=rows, metadata=meta)
|