Metadata-Version: 2.4
Name: jintel
Version: 0.28.0
Summary: Python client for the Jintel intelligence API
Project-URL: Homepage, https://github.com/YojinHQ/jintel-py
Project-URL: Repository, https://github.com/YojinHQ/jintel-py
Project-URL: Issues, https://github.com/YojinHQ/jintel-py/issues
Author-email: Yojin <support@yojin.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: api-client,financial-data,graphql,jintel,market-data
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx[http2]<1.0,>=0.27
Requires-Dist: pydantic<3.0,>=2.7
Description-Content-Type: text/markdown

# jintel

Python client for the [Jintel](https://api.jintel.ai) intelligence API. Typed queries, Pydantic-validated responses, an optional response cache, and a dynamic query builder that threads per-field filters into the underlying GraphQL.

Mirrors [`@yojinhq/jintel-client`](https://github.com/YojinHQ/jintel-sdk/tree/main/packages/client) section-for-section.

## Install

```bash
pip install jintel
```

Requires Python 3.10+.

## Quick start

```python
import os
from jintel import JintelClient

with JintelClient(api_key=os.environ["JINTEL_API_KEY"]) as jintel:
    quotes = jintel.quotes(["AAPL", "MSFT", "BTC"])
    aapl = jintel.enrich_entity("AAPL", fields=["market", "news", "analyst"])
```

Async twin — same surface, `await`-able:

```python
import asyncio, os
from jintel import AsyncJintelClient

async def main() -> None:
    async with AsyncJintelClient(api_key=os.environ["JINTEL_API_KEY"]) as jintel:
        quotes = await jintel.quotes(["AAPL"])

asyncio.run(main())
```

All public methods return `JintelResult[T]` — a discriminated union `Ok[T] | Err` — so errors never throw. Pattern-match instead of `try/except`:

```python
from jintel import Ok, Err

result = jintel.quotes(["AAPL"])
match result:
    case Ok(data=quotes):
        for q in quotes:
            if q is None:
                continue  # PIT UNSUPPORTED slot
            print(q.ticker, q.price)
    case Err(error=msg):
        log.warning("quotes failed: %s", msg)
```

For arbitrary GraphQL, use `jintel.request(query, variables)` (raises) or `jintel.raw_request(query, variables)` (returns the full envelope including extensions).

## Filtering array sub-graphs

Most array fields accept an optional dedicated filter. The generic `ArrayFilterInput` covers date range + limit + sort; many sub-graphs have domain-specific inputs with extra dimensions.

```python
from jintel import EnrichOptions
from jintel.filters import (
    ArrayFilterInput, NewsFilterInput, ExecutivesFilterInput,
    InsiderTradeFilterInput, EarningsFilterInput, SegmentRevenueFilterInput,
    TopHoldersFilterInput, FinancialStatementFilterInput,
    SanctionsFilterInput, CampaignFinanceFilterInput, FilingsFilterInput,
    RiskSignalFilterInput, OptionsChainFilterInput, FuturesCurveFilterInput,
    SortDirection,
)
from jintel.types import (
    AcquisitionDirection, ExecutiveSort, FilingType, OptionsChainSort,
    OptionType, RiskSignalType, Severity,
)

# Generic ArrayFilterInput — research, predictions, discussions, social,
# institutionalHoldings, earningsPressReleases, periodicFilings,
# market.history/keyEvents/shortInterest, economics.
jintel.enrich_entity("AAPL", fields=["research", "market"], options=EnrichOptions(
    filter=ArrayFilterInput(since="2025-01-01", limit=10, sort=SortDirection.DESC),
))

# NewsFilterInput — filter by source and sentiment score
jintel.enrich_entity("AAPL", fields=["news"], options=EnrichOptions(
    news_filter=NewsFilterInput(min_sentiment=0, limit=10),
))

# ExecutivesFilterInput — top-paid officers only
jintel.enrich_entity("AAPL", fields=["executives"], options=EnrichOptions(
    executives_filter=ExecutivesFilterInput(
        min_pay=1_000_000, sort_by=ExecutiveSort.PAY_DESC, limit=5
    ),
))

# InsiderTradeFilterInput — directors only, acquisitions >= $100k
jintel.enrich_entity("AAPL", fields=["insiderTrades"], options=EnrichOptions(
    insider_trades_filter=InsiderTradeFilterInput(
        is_director=True,
        acquired_disposed=AcquisitionDirection.ACQUIRED,
        min_value=100_000,
    ),
))

# EarningsFilterInput — reported beats >= 5%
jintel.enrich_entity("AAPL", fields=["earnings"], options=EnrichOptions(
    earnings_filter=EarningsFilterInput(only_reported=True, min_surprise_percent=5),
))

# SegmentRevenueFilterInput — product breakdown >= $1B
jintel.enrich_entity("AAPL", fields=["segmentedRevenue"], options=EnrichOptions(
    segmented_revenue_filter=SegmentRevenueFilterInput(
        dimensions=["PRODUCT"], min_value=1_000_000_000
    ),
))

# TopHoldersFilterInput — paginated top-holder lookup
jintel.enrich_entity("AAPL", fields=["topHolders"], options=EnrichOptions(
    top_holders_filter=TopHoldersFilterInput(limit=25, offset=0, min_value=50_000),
))

# FinancialStatementFilterInput — annual only
jintel.enrich_entity("AAPL", fields=["financials"], options=EnrichOptions(
    financial_statements_filter=FinancialStatementFilterInput(
        period_types=["12M"], limit=5
    ),
))

# SanctionsFilterInput + CampaignFinanceFilterInput — on regulatory
jintel.enrich_entity("Gazprom", fields=["regulatory"], options=EnrichOptions(
    sanctions_filter=SanctionsFilterInput(min_score=80, programs=["SDGT"]),
    campaign_finance_filter=CampaignFinanceFilterInput(cycle=2024, party="DEM"),
))

# FilingsFilterInput — narrow SEC filings by form type
jintel.enrich_entity("AAPL", fields=["regulatory"], options=EnrichOptions(
    filings_filter=FilingsFilterInput(
        types=[FilingType.FILING_10K, FilingType.FILING_10Q], limit=5
    ),
))

# RiskSignalFilterInput — drop low-severity noise
jintel.enrich_entity("Gazprom", fields=["risk"], options=EnrichOptions(
    risk_signal_filter=RiskSignalFilterInput(severities=[Severity.HIGH, Severity.CRITICAL]),
))

# OptionsChainFilterInput — chains can exceed 5 000 rows; filter aggressively
jintel.enrich_entity("BTC", fields=["derivatives"], options=EnrichOptions(
    options_filter=OptionsChainFilterInput(
        option_type=OptionType.CALL,
        strike_min=60_000, strike_max=80_000,
        min_open_interest=100,
        sort=OptionsChainSort.VOLUME_DESC,
        limit=25,
    ),
))

# FuturesCurveFilterInput — defaults to ASC (nearest contract first)
jintel.enrich_entity("BTC", fields=["derivatives"], options=EnrichOptions(
    futures_filter=FuturesCurveFilterInput(limit=10),
))
```

Each filter option applies to one sub-graph only, so you can mix them in a single request. The generic `filter` no longer applies to fields that migrated to domain-specific inputs (`news`, `executives`, `insiderTrades`, `earnings`, `segmentedRevenue`, `topHolders`, `financials.*`, `regulatory.sanctions`, `regulatory.campaignFinance`) — use the dedicated option for those, or the client raises `JintelValidationError` at call time.

### Top-level queries

Economics and short-interest queries accept the generic `ArrayFilterInput`. `sanctions_screen` and `campaign_finance` accept their domain-specific filters:

```python
from jintel.types import GdpType
from jintel.filters import ArrayFilterInput, SanctionsFilterInput, CampaignFinanceFilterInput

jintel.gdp("USA", type=GdpType.REAL,
           filter=ArrayFilterInput(since="2010-01-01", limit=20))
jintel.inflation("USA", filter=ArrayFilterInput(since="2020-01-01"))
jintel.short_interest("GME", filter=ArrayFilterInput(limit=5))

# Root sanctions screen — filter by score, list, or program
jintel.sanctions_screen(
    "Gazprom", country="RU",
    filter=SanctionsFilterInput(min_score=80, list_names=["SDN"]),
)

# Root campaign finance — narrow to party / state / cycle
jintel.campaign_finance(
    "Acme PAC", cycle=2024,
    filter=CampaignFinanceFilterInput(party="DEM", state="CA", min_raised=100_000),
)

# US macro economic series
jintel.macro_series("UNRATE",
                    filter=ArrayFilterInput(since="2000-01-01", limit=300))
jintel.macro_series_batch(["GDPC1", "CPIAUCSL"],
                          filter=ArrayFilterInput(since="2010-01-01"))
```

### Defaults

| Filter | Default limit | Default sort |
| --- | --- | --- |
| `ArrayFilterInput` | 20 | `DESC` |
| `NewsFilterInput` | 20 | `DESC` (by date) |
| `ExecutivesFilterInput` | 20 | `PAY_DESC` |
| `InsiderTradeFilterInput` | 20 | `DESC` (by transactionDate) |
| `EarningsFilterInput` | 20 | `DESC` (by reportDate) |
| `SegmentRevenueFilterInput` | 20 | `DESC` (by filingDate) |
| `TopHoldersFilterInput` | 20 (offset 0) | `DESC` (by value) |
| `FinancialStatementFilterInput` | 20 | `DESC` (by periodEnding) |
| `SanctionsFilterInput` | 20 | `DESC` (by score) |
| `CampaignFinanceFilterInput` | 20 | `DESC` (by totalRaised) |
| `FilingsFilterInput` | 20 | `DESC` |
| `RiskSignalFilterInput` | 20 | `DESC` |
| `FuturesCurveFilterInput` | 50 | `ASC` |
| `OptionsChainFilterInput` | 100 | `EXPIRATION_ASC` |
| `ClinicalTrialFilterInput` | 20 | `DESC` (by startDate) |
| `FdaEventFilterInput` | 20 | `DESC` (by reportDate) |
| `LitigationFilterInput` | 20 | `DESC` (by dateFiled) |
| `GovernmentContractFilterInput` | 20 | `DESC` (by actionDate) |

Omitting a filter on a sub-graph returns the full upstream set with that input's defaults applied.

### Breaking changes in 0.21

- `top_holders: { limit, offset }` option **removed**. Use `top_holders_filter=TopHoldersFilterInput(...)` instead.
- The generic `filter` option no longer threads into `news`, `insiderTrades`, `earnings`, `segmentedRevenue`, or `financials.*` — use the new domain-specific filter options above. The Python client raises `JintelValidationError` at call time if you mix them.

## Batch enrichment

`batch_enrich` accepts up to 20 tickers and pushes server-side loaders to batch and deduplicate upstream calls:

```python
batch = jintel.batch_enrich(
    ["AAPL", "MSFT", "GOOG"],
    fields=["market", "news", "technicals"],
    options=EnrichOptions(filter=ArrayFilterInput(limit=5)),
)
```

## Point-in-time queries (`as_of`)

Pass `as_of` (ISO 8601) to bound a query to data that was knowable at that timestamp — no lookahead bias in backtests. Set it per call or as a client-wide default.

```python
# Per-call — overrides any default.
aapl_last_summer = jintel.batch_enrich(
    ["AAPL"],
    fields=["news", "institutionalHoldings"],
    options=EnrichOptions(
        as_of="2023-08-15T00:00:00Z",
        filter=ArrayFilterInput(limit=5),
    ),
)

# Or once at construction — locks the entire client to a replay date.
replay = JintelClient(
    api_key=os.environ["JINTEL_API_KEY"],
    as_of="2023-08-15T00:00:00Z",
)
```

Every PIT response carries `extensions.asOf.fields` with the per-field policy:

```python
from jintel import parse_as_of_extension

envelope = jintel.raw_request(
    """query Q($t: [String!]!, $a: String) {
         quotes(tickers: $t, asOf: $a) { ticker }
       }""",
    {"t": ["AAPL"], "a": "2023-08-15T00:00:00Z"},
)
ext = parse_as_of_extension(envelope)
if ext is not None:
    for path, policy in ext.fields.items():
        print(path, policy.klass)
# Entity.market.quote → AsOfFieldClass.UNSUPPORTED
```

`SUPPORTED` fields honor `as_of` honestly. `BEST_EFFORT` fields run with a documented caveat. `UNSUPPORTED` fields (live quotes, current fundamentals, OFAC SDN, derivatives, etc.) return `null`/`[]` rather than serve current data — `quotes()` returns `list[MarketQuote | None]` so callers must handle the gap. Cached responses are bucketed by `as_of`, so PIT and live requests never share a slot.

## Response caching

Pass `cache=True` to enable an in-process TTL cache (30 s for quotes, 5 min for enrich / price history). Eliminates redundant HTTP when the same data is requested in a short window.

```python
from jintel import JintelClient, CacheConfig

jintel = JintelClient(
    api_key=os.environ["JINTEL_API_KEY"],
    cache=CacheConfig(quotes_ttl_ms=15_000, enrich_ttl_ms=120_000),
)

jintel.invalidate_cache(["AAPL"])  # after an external signal event
print(jintel.cache_stats())  # CacheStats(hits=12, misses=3, evictions=0, size=15)
```

The cache is in-memory by default. To plug a Redis-backed implementation, satisfy the `CacheBackend` Protocol and pass it as `cache_backend=...` (the SDK does not ship a Redis backend in v0).

## Pattern-matching `JintelResult`

Python's `match`/`case` makes the discriminated union ergonomic — Python-specific addition over the TS surface:

```python
from jintel import Ok, Err

match jintel.gdp("USA"):
    case Ok(data=points):
        latest = points[0]
        print(f"GDP {latest.date}: {latest.value}")
    case Err(error=msg):
        print(f"GDP query failed: {msg}")
```

`.unwrap()` returns the data or raises `JintelError`. `.map(fn)` transforms the success branch. `.value_or(default)` returns the data or a fallback.

## License

MIT
