Skip to main content

Python client for the Jintel intelligence API

Project description

jintel

Python client for the Jintel 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 section-for-section.

Install

pip install jintel

Requires Python 3.10+.

Quick start

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:

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:

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.

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:

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:

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.

# 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:

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.

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).

Payment modes (Bearer + x402)

Two ways to pay for queries on the same endpoint:

Bearer (prepaid credits) — pass api_key and you're done. This is the default mode and matches the TS client.

x402 (per-call USDC on Base) — pass a payment_signer callable. On any 402 Payment Required response, the SDK decodes the PAYMENT-REQUIRED header into a typed X402Quote, calls your signer to produce a base64 EIP-3009 USDC authorization, retries the request with PAYMENT-SIGNATURE, then exposes the PAYMENT-RESPONSE settlement record on client.last_settlement.

from jintel import JintelClient, X402Quote

def my_signer(quote: X402Quote) -> str:
    # Use eth-account, web3.py, or an MCP wallet — produce a base64 PaymentPayload.
    ...

# x402-only — no Bearer key needed
with JintelClient(payment_signer=my_signer) as jintel:
    result = jintel.quotes(["AAPL"])
    print(jintel.last_settlement)  # X402SettlementResponse | None

# Both modes — Bearer settles by default; x402 fires on CREDITS_EXHAUSTED
with JintelClient(api_key=os.environ["JINTEL_API_KEY"], payment_signer=my_signer) as jintel:
    ...

When you don't supply a signer, a 402 raises JintelPaymentRequiredError(quote: X402Quote) so callers can handle the payment manually. When the Bearer path runs out of credits, the server returns the GraphQL error code CREDITS_EXHAUSTED, which the SDK surfaces as JintelCreditsExhaustedError.

This SDK does not embed wallet logic — payment_signer is the integration seam. See examples/x402_payment.py for a worked example using eth-account.

Pattern-matching JintelResult

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

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jintel-0.29.0.tar.gz (40.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jintel-0.29.0-py3-none-any.whl (44.2 kB view details)

Uploaded Python 3

File details

Details for the file jintel-0.29.0.tar.gz.

File metadata

  • Download URL: jintel-0.29.0.tar.gz
  • Upload date:
  • Size: 40.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jintel-0.29.0.tar.gz
Algorithm Hash digest
SHA256 eeaf29dd0290591734a0020bf78f5a5f18cad2d860dd043c1ae4fc2308328c7a
MD5 4a3e13258b64ffda1392f4a27bb4de6a
BLAKE2b-256 6249d78fa2ee5fe9d2b7d0ebab03ccd92a58d2112ba8347d3f0047d7c62964e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for jintel-0.29.0.tar.gz:

Publisher: release.yml on YojinHQ/jintel-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file jintel-0.29.0-py3-none-any.whl.

File metadata

  • Download URL: jintel-0.29.0-py3-none-any.whl
  • Upload date:
  • Size: 44.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jintel-0.29.0-py3-none-any.whl
Algorithm Hash digest
SHA256 97e264023229e2ac4b5ee0793b5a3d3eaade1a23b3aa3e05fe34fe864df537ee
MD5 9026f3ad05bc8abb3af42df51c3ea7ea
BLAKE2b-256 616055ffdfe629f6aa5f29c0c0c95c8ed23319d0463868dd931dc79ec6ea7e27

See more details on using hashes here.

Provenance

The following attestation bundles were made for jintel-0.29.0-py3-none-any.whl:

Publisher: release.yml on YojinHQ/jintel-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page