outerproduct 0.2.3__tar.gz → 0.3.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.

Potentially problematic release.


This version of outerproduct might be problematic. Click here for more details.

Files changed (33) hide show
  1. {outerproduct-0.2.3 → outerproduct-0.3.0}/PKG-INFO +55 -15
  2. {outerproduct-0.2.3 → outerproduct-0.3.0}/README.md +51 -14
  3. {outerproduct-0.2.3 → outerproduct-0.3.0}/pyproject.toml +22 -9
  4. outerproduct-0.3.0/src/outerproduct/__init__.py +362 -0
  5. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/agentic/documents.py +40 -13
  6. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/client/__init__.py +31 -8
  7. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/client/_http.py +27 -1
  8. outerproduct-0.3.0/src/outerproduct/client/apis.py +487 -0
  9. outerproduct-0.3.0/src/outerproduct/client/client.py +442 -0
  10. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/client/exceptions.py +3 -4
  11. outerproduct-0.3.0/src/outerproduct/connector.py +288 -0
  12. outerproduct-0.3.0/src/outerproduct/dataset/__init__.py +377 -0
  13. outerproduct-0.3.0/src/outerproduct/dataset/datatypes.py +122 -0
  14. outerproduct-0.3.0/src/outerproduct/expr.py +200 -0
  15. outerproduct-0.3.0/src/outerproduct/id.py +3 -0
  16. outerproduct-0.3.0/src/outerproduct/job.py +240 -0
  17. outerproduct-0.3.0/src/outerproduct/job_result.py +72 -0
  18. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/model.py +326 -139
  19. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/reasoning/__init__.py +133 -101
  20. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/reasoning/pattern_tracker.py +87 -66
  21. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/trainer.py +78 -155
  22. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/utils.py +90 -5
  23. outerproduct-0.2.3/src/outerproduct/__init__.py +0 -173
  24. outerproduct-0.2.3/src/outerproduct/client/apis.py +0 -319
  25. outerproduct-0.2.3/src/outerproduct/client/client.py +0 -240
  26. outerproduct-0.2.3/src/outerproduct/client/endpoints.py +0 -311
  27. outerproduct-0.2.3/src/outerproduct/connector.py +0 -745
  28. outerproduct-0.2.3/src/outerproduct/dataset.py +0 -460
  29. outerproduct-0.2.3/src/outerproduct/job.py +0 -125
  30. {outerproduct-0.2.3 → outerproduct-0.3.0}/.gitignore +0 -0
  31. {outerproduct-0.2.3 → outerproduct-0.3.0}/LICENSE +0 -0
  32. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/agentic/__init__.py +0 -0
  33. {outerproduct-0.2.3 → outerproduct-0.3.0}/src/outerproduct/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: outerproduct
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for the OuterProduct API
5
5
  Project-URL: Documentation, https://docs.outerproduct.com
6
6
  Project-URL: Homepage, https://outerproduct.com
@@ -21,11 +21,14 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.12
23
23
  Requires-Dist: httpx~=0.27
24
+ Requires-Dist: narwhals>=1.13
24
25
  Requires-Dist: numpy~=1.23
25
26
  Requires-Dist: outerproduct-http-types
26
27
  Requires-Dist: pandas~=2.0
28
+ Requires-Dist: polars<=1.41.0
27
29
  Requires-Dist: pydantic<3,>=2.0
28
30
  Provides-Extra: dev
31
+ Requires-Dist: anyio; extra == 'dev'
29
32
  Requires-Dist: pyarrow>=14.0; extra == 'dev'
30
33
  Requires-Dist: pytest; extra == 'dev'
31
34
  Requires-Dist: respx; extra == 'dev'
@@ -34,10 +37,17 @@ Requires-Dist: pyarrow>=14.0; extra == 'parquet'
34
37
  Description-Content-Type: text/markdown
35
38
 
36
39
  # outerproduct-sdk
40
+
37
41
  Python SDK for training and explaining tabular ML models with the OuterProduct API.
38
42
 
39
43
  ## Installation
40
44
 
45
+ ```bash
46
+ uv add outerproduct
47
+ ```
48
+
49
+ Or with pip:
50
+
41
51
  ```bash
42
52
  pip install outerproduct
43
53
  ```
@@ -51,11 +61,10 @@ import outerproduct as op
51
61
  op.init(api_key="op_live_...")
52
62
 
53
63
  # 2. Upload data
54
- connector = op.FileUploadConnector()
55
- dataset = connector.upload("credit.csv", label_column="default")
64
+ dataset = op.LocalDataset.from_csv("credit.csv").upload()
56
65
 
57
- # 3. Train a reasoning model
58
- rm = op.reasoning.fit(dataset)
66
+ # 3. Train a reasoning model (returns a job handle; .wait() blocks for the model)
67
+ rm = op.reasoning.fit(dataset).wait()
59
68
 
60
69
  # 4. Predict
61
70
  predictions = rm.predict(X_new)
@@ -80,11 +89,37 @@ df = pd.read_csv("credit.csv")
80
89
  dataset = op.Dataset.from_pandas(df, label_column="default")
81
90
  ```
82
91
 
92
+ ### Tabularize Documents into a Dataset
93
+
94
+ ```python
95
+ from outerproduct.agentic import documents
96
+
97
+ docs = documents.DocumentSet.from_directory("./invoices")
98
+ refs = documents.upload(docs)
99
+ schema = documents.induce_schema(refs, use_case="fraud audit", skill="invoice")
100
+ dataset = documents.tabularize(refs, schema, label_column="is_fraud")
101
+ ```
102
+
103
+ You can also hand-author the schema:
104
+
105
+ ```python
106
+ from outerproduct.agentic.documents import DocumentSchema, DocumentQuestion, AnswerType
107
+
108
+ schema = DocumentSchema(
109
+ skill="invoice",
110
+ use_case="fraud audit",
111
+ questions=[
112
+ DocumentQuestion(id="invoice_total", question="Total amount due?", answer_type=AnswerType.NUMBER, unit="USD"),
113
+ DocumentQuestion(id="payment_terms", question="Payment terms?", answer_type=AnswerType.ENUM, enum=("net_15", "net_30", "net_60")),
114
+ ],
115
+ )
116
+ ```
117
+
83
118
  ### Train a Plain Model (No Explanations)
84
119
 
85
120
  ```python
86
121
  trainer = op.Trainer.configure(dataset, model_types=["tabm", "xgboost"])
87
- model = trainer.run(strategy="random")
122
+ model = trainer.run(strategy="random").wait()
88
123
  predictions = model.predict(X_new)
89
124
  ```
90
125
 
@@ -98,14 +133,14 @@ teacher = op.model.Predictor(
98
133
  )
99
134
 
100
135
  # Distill into a reasoning model
101
- rm = op.reasoning.fit(dataset, teacher=teacher)
136
+ rm = op.reasoning.fit(dataset, teacher=teacher).wait()
102
137
  ```
103
138
 
104
139
  You can also distill an existing OuterProduct-trained model:
105
140
 
106
141
  ```python
107
- trained = op.Trainer.configure(dataset).run()
108
- rm = op.reasoning.fit(dataset, teacher=trained)
142
+ trained = op.Trainer.configure(dataset).run().wait()
143
+ rm = op.reasoning.fit(dataset, teacher=trained).wait()
109
144
  ```
110
145
 
111
146
  ### Reasoning Model Operations
@@ -125,16 +160,23 @@ trace = rm.scenario(X, target_class=1, max_steps=10)
125
160
 
126
161
  # Pattern aggregation across rejected predictions
127
162
  import outerproduct as op
128
- pt = op.reasoning.pattern_tracker.fit(rm, dataset, target_range=(0.5, None))
163
+ pt = op.reasoning.pattern_tracker.fit(rm, dataset, target_range=(0.5, None)).wait()
129
164
  pt.distribution(X_new)
130
165
  ```
131
166
 
132
- ### Non-Blocking Jobs
167
+ ### Jobs
133
168
 
134
- `reasoning.fit()` blocks until training completes by default. To return immediately:
169
+ Every async server operation — `Trainer.run()`, `reasoning.fit()`,
170
+ `pattern_tracker.fit()` — submits the work and returns a typed job handle
171
+ (`TrainerJob`, `ReasoningFitJob`, `PatternTrackerJob`). Drive it however you
172
+ need:
135
173
 
136
174
  ```python
137
- rm = op.reasoning.fit(dataset, non_blocking=True)
175
+ job = op.reasoning.fit(dataset)
176
+
177
+ job.status() # "pending" | "running" | "completed" | "failed"
178
+ job.results() # typed JobResult once finished (raises JobFailed on failure)
179
+ rm = job.wait() # block until completion, return the trained ReasoningModel
138
180
  ```
139
181
 
140
182
  ### Configuration
@@ -149,7 +191,5 @@ op.init()
149
191
  op.init(
150
192
  api_key="op_live_...",
151
193
  base_url="https://api.outerproduct.com",
152
- hardware=op.ModalHardware(),
153
194
  )
154
195
  ```
155
-
@@ -1,8 +1,15 @@
1
1
  # outerproduct-sdk
2
+
2
3
  Python SDK for training and explaining tabular ML models with the OuterProduct API.
3
4
 
4
5
  ## Installation
5
6
 
7
+ ```bash
8
+ uv add outerproduct
9
+ ```
10
+
11
+ Or with pip:
12
+
6
13
  ```bash
7
14
  pip install outerproduct
8
15
  ```
@@ -16,11 +23,10 @@ import outerproduct as op
16
23
  op.init(api_key="op_live_...")
17
24
 
18
25
  # 2. Upload data
19
- connector = op.FileUploadConnector()
20
- dataset = connector.upload("credit.csv", label_column="default")
26
+ dataset = op.LocalDataset.from_csv("credit.csv").upload()
21
27
 
22
- # 3. Train a reasoning model
23
- rm = op.reasoning.fit(dataset)
28
+ # 3. Train a reasoning model (returns a job handle; .wait() blocks for the model)
29
+ rm = op.reasoning.fit(dataset).wait()
24
30
 
25
31
  # 4. Predict
26
32
  predictions = rm.predict(X_new)
@@ -45,11 +51,37 @@ df = pd.read_csv("credit.csv")
45
51
  dataset = op.Dataset.from_pandas(df, label_column="default")
46
52
  ```
47
53
 
54
+ ### Tabularize Documents into a Dataset
55
+
56
+ ```python
57
+ from outerproduct.agentic import documents
58
+
59
+ docs = documents.DocumentSet.from_directory("./invoices")
60
+ refs = documents.upload(docs)
61
+ schema = documents.induce_schema(refs, use_case="fraud audit", skill="invoice")
62
+ dataset = documents.tabularize(refs, schema, label_column="is_fraud")
63
+ ```
64
+
65
+ You can also hand-author the schema:
66
+
67
+ ```python
68
+ from outerproduct.agentic.documents import DocumentSchema, DocumentQuestion, AnswerType
69
+
70
+ schema = DocumentSchema(
71
+ skill="invoice",
72
+ use_case="fraud audit",
73
+ questions=[
74
+ DocumentQuestion(id="invoice_total", question="Total amount due?", answer_type=AnswerType.NUMBER, unit="USD"),
75
+ DocumentQuestion(id="payment_terms", question="Payment terms?", answer_type=AnswerType.ENUM, enum=("net_15", "net_30", "net_60")),
76
+ ],
77
+ )
78
+ ```
79
+
48
80
  ### Train a Plain Model (No Explanations)
49
81
 
50
82
  ```python
51
83
  trainer = op.Trainer.configure(dataset, model_types=["tabm", "xgboost"])
52
- model = trainer.run(strategy="random")
84
+ model = trainer.run(strategy="random").wait()
53
85
  predictions = model.predict(X_new)
54
86
  ```
55
87
 
@@ -63,14 +95,14 @@ teacher = op.model.Predictor(
63
95
  )
64
96
 
65
97
  # Distill into a reasoning model
66
- rm = op.reasoning.fit(dataset, teacher=teacher)
98
+ rm = op.reasoning.fit(dataset, teacher=teacher).wait()
67
99
  ```
68
100
 
69
101
  You can also distill an existing OuterProduct-trained model:
70
102
 
71
103
  ```python
72
- trained = op.Trainer.configure(dataset).run()
73
- rm = op.reasoning.fit(dataset, teacher=trained)
104
+ trained = op.Trainer.configure(dataset).run().wait()
105
+ rm = op.reasoning.fit(dataset, teacher=trained).wait()
74
106
  ```
75
107
 
76
108
  ### Reasoning Model Operations
@@ -90,16 +122,23 @@ trace = rm.scenario(X, target_class=1, max_steps=10)
90
122
 
91
123
  # Pattern aggregation across rejected predictions
92
124
  import outerproduct as op
93
- pt = op.reasoning.pattern_tracker.fit(rm, dataset, target_range=(0.5, None))
125
+ pt = op.reasoning.pattern_tracker.fit(rm, dataset, target_range=(0.5, None)).wait()
94
126
  pt.distribution(X_new)
95
127
  ```
96
128
 
97
- ### Non-Blocking Jobs
129
+ ### Jobs
98
130
 
99
- `reasoning.fit()` blocks until training completes by default. To return immediately:
131
+ Every async server operation — `Trainer.run()`, `reasoning.fit()`,
132
+ `pattern_tracker.fit()` — submits the work and returns a typed job handle
133
+ (`TrainerJob`, `ReasoningFitJob`, `PatternTrackerJob`). Drive it however you
134
+ need:
100
135
 
101
136
  ```python
102
- rm = op.reasoning.fit(dataset, non_blocking=True)
137
+ job = op.reasoning.fit(dataset)
138
+
139
+ job.status() # "pending" | "running" | "completed" | "failed"
140
+ job.results() # typed JobResult once finished (raises JobFailed on failure)
141
+ rm = job.wait() # block until completion, return the trained ReasoningModel
103
142
  ```
104
143
 
105
144
  ### Configuration
@@ -114,7 +153,5 @@ op.init()
114
153
  op.init(
115
154
  api_key="op_live_...",
116
155
  base_url="https://api.outerproduct.com",
117
- hardware=op.ModalHardware(),
118
156
  )
119
157
  ```
120
-
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "outerproduct"
3
- version = "0.2.3"
3
+ version = "0.3.0"
4
4
  description = "Python SDK for the OuterProduct API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -25,10 +25,12 @@ classifiers = [
25
25
  ]
26
26
  dependencies = [
27
27
  "httpx~=0.27",
28
+ "narwhals>=1.13",
28
29
  "numpy~=1.23",
29
30
  "outerproduct-http-types",
30
31
  "pandas~=2.0",
31
- "pydantic>=2.0,<3"
32
+ "polars<=1.41.0",
33
+ "pydantic>=2.0,<3",
32
34
  ]
33
35
 
34
36
  [project.urls]
@@ -36,11 +38,12 @@ Documentation = "https://docs.outerproduct.com"
36
38
  Homepage = "https://outerproduct.com"
37
39
 
38
40
  [project.optional-dependencies]
39
- dev = ["pytest", "respx", "pyarrow>=14.0"]
41
+ dev = ["anyio", "pytest", "respx", "pyarrow>=14.0"]
40
42
  parquet = ["pyarrow>=14.0"]
41
43
 
42
44
  [dependency-groups]
43
45
  dev = [
46
+ "anyio>=4.0",
44
47
  "prek>=0.3.11",
45
48
  "pyarrow>=14.0",
46
49
  "pytest>=9.0.3",
@@ -66,7 +69,7 @@ include = [
66
69
  ]
67
70
 
68
71
  [tool.hatch.envs.hatch-test]
69
- extra-dependencies = ["respx", "pyarrow>=14.0"]
72
+ extra-dependencies = ["anyio", "respx", "pyarrow>=14.0"]
70
73
 
71
74
  [[tool.hatch.envs.hatch-test.matrix]]
72
75
  python = ["3.12", "3.13", "3.14"]
@@ -108,8 +111,18 @@ exclude = [".agents/**", ".ruff_cache/**", ".venv/**"]
108
111
  [tool.ty.src]
109
112
  include = ["src"]
110
113
 
111
- [tool.uv.sources]
112
- # Track outerproduct-http-types from main so the SDK doesn't lag behind
113
- # wire-format changes that haven't been published to PyPI yet. Pin by tag
114
- # = "v0.3.0" once http-types resumes a stable release cadence.
115
- outerproduct-http-types = { git = "https://github.com/OuterProduct-ai/outerproduct-http-types.git", branch = "main" }
114
+ # By default the SDK resolves outerproduct-http-types from PyPI, via the plain
115
+ # `outerproduct-http-types` entry in [project].dependencies above — pure-PyPI,
116
+ # nothing to configure and no token needed for `uv sync`.
117
+ #
118
+ # Co-developing http-types alongside the SDK? Uncomment [tool.uv.sources] and
119
+ # ONE override below to resolve it from source instead. This is a uv-only
120
+ # setting that is stripped at build time, so it never affects the published
121
+ # wheel. NOTE: with the git form, check-main-pins.py requires `branch = "main"`
122
+ # on merges to main — don't commit a non-main pin (or the local path) to main.
123
+ #
124
+ # [tool.uv.sources]
125
+ # # tip of http-types' default branch (CI git-auth is set up for this private repo):
126
+ # outerproduct-http-types = { git = "https://github.com/OuterProduct-ai/outerproduct-http-types.git", branch = "main" }
127
+ # # ...or a local editable checkout for live co-editing:
128
+ # outerproduct-http-types = { path = "../outerproduct-http-types", editable = true }
@@ -0,0 +1,362 @@
1
+ """OuterProduct — a Python SDK for training and explaining tabular ML models.
2
+
3
+ The package exposes a small, flat surface organised around the following
4
+ core concepts:
5
+
6
+ - :class:`~outerproduct.dataset.Dataset` — a tabular dataset with an optional label column and
7
+ per-column schema (:class:`~outerproduct_http_types.DatasetColumn`).
8
+ - :class:`~outerproduct.connector.Connector` — an *external* source of data
9
+ (:class:`~outerproduct.connector.S3Connector`, …) that produces a :class:`~outerproduct.dataset.Dataset`;
10
+ local files and in-memory frames are staged via :meth:`~outerproduct.dataset.LocalDataset.upload` instead.
11
+ - :class:`~outerproduct.trainer.Trainer` — orchestrates general-purpose training and
12
+ returns a :class:`~outerproduct.model.Model` via :meth:`~outerproduct.trainer.Trainer.configure` and
13
+ :meth:`~outerproduct.trainer.Trainer.run`.
14
+ - :class:`~outerproduct.model.Model` — a trained predictor with :meth:`~outerproduct.model.Model.predict`.
15
+ - :class:`~outerproduct.model.Predictor` — a duck-typed wrapper around an external
16
+ prediction endpoint, used to hand black-box models into
17
+ :func:`~outerproduct.reasoning.fit` for distillation.
18
+ - :class:`~outerproduct.model.ReasoningModel` — a :class:`~outerproduct.model.Model` that also supports
19
+ :meth:`~outerproduct.model.ReasoningModel.explain`, :meth:`~outerproduct.model.ReasoningModel.get_global_drivers`,
20
+ and :meth:`~outerproduct.model.ReasoningModel.scenario`.
21
+ Produced by :func:`~outerproduct.reasoning.fit`.
22
+ - :class:`~outerproduct.reasoning.pattern_tracker.PatternTracker` —
23
+ aggregates the per-row local-rule explanations a
24
+ :class:`~outerproduct.model.ReasoningModel` produces over a target
25
+ prediction band into a small set of executable filter patterns.
26
+ - :class:`~outerproduct.reasoning.Reasoning` / :class:`~outerproduct.reasoning.ScenarioResult` — the typed results of
27
+ reasoning calls.
28
+
29
+ Quickstart
30
+ ----------
31
+ .. code-block:: python
32
+
33
+ import outerproduct as op
34
+
35
+ op.init(api_key="...")
36
+ dataset = op.LocalDataset.from_csv("customers.csv").upload()
37
+ reasoning_model = op.reasoning.fit(dataset, task=op.BinclassConfig(label_column="churn"))
38
+ test_dataset = op.Dataset(X_new)
39
+ predictions = reasoning_model.predict(test_dataset)
40
+ reasoning = reasoning_model.explain(test_dataset)
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ from contextlib import contextmanager
46
+ from contextvars import ContextVar
47
+ from dataclasses import dataclass, field
48
+ from importlib.metadata import version as _pkg_version
49
+ from typing import TYPE_CHECKING, Generator
50
+
51
+ from outerproduct_http_types import (
52
+ BinclassConfig,
53
+ ConnectorType,
54
+ ForecastingConfig,
55
+ MulticlassConfig,
56
+ RegressionConfig,
57
+ SequenceConfig,
58
+ TaskConfig,
59
+ TaskKind,
60
+ )
61
+
62
+ from outerproduct import agentic
63
+ from outerproduct.connector import (
64
+ Connector,
65
+ DatabricksConnector,
66
+ S3Connector,
67
+ SnowflakeConnector,
68
+ )
69
+ from outerproduct.dataset import (
70
+ Dataset,
71
+ DatasetColumn,
72
+ Datatype,
73
+ LocalDataset,
74
+ )
75
+ from outerproduct.expr import Expr, col
76
+ from outerproduct.job import Job, JobFailed, JobNotReady, ReasoningFitJob, TrainerJob
77
+ from outerproduct.job_result import JobResult
78
+ from outerproduct.model import (
79
+ DiffModel,
80
+ Model,
81
+ ReasoningModel,
82
+ SequenceReasoningModel,
83
+ )
84
+ from outerproduct import reasoning
85
+ from outerproduct.reasoning import (
86
+ Change,
87
+ Counterfactual,
88
+ QueryResult,
89
+ Reasoning,
90
+ ScenarioResult,
91
+ )
92
+ from outerproduct.reasoning import pattern_tracker # noqa: F401 — makes op.reasoning.pattern_tracker accessible
93
+ from outerproduct.trainer import (
94
+ Metric,
95
+ Trainer,
96
+ )
97
+
98
+ if TYPE_CHECKING:
99
+ from outerproduct.client import AsyncOuterProductClient, OuterProductClient
100
+
101
+ try:
102
+ __version__ = _pkg_version("outerproduct")
103
+ except Exception:
104
+ __version__ = "unknown"
105
+
106
+
107
+ @dataclass
108
+ class _GlobalState:
109
+ """Module-level state populated by :func:`~outerproduct.init`. Internal."""
110
+
111
+ api_key: str | None = None
112
+ base_url: str | None = None
113
+ extra: dict[str, object] = field(default_factory=dict)
114
+
115
+
116
+ _state = _GlobalState()
117
+
118
+
119
+ # ---------------------------------------------------------------------
120
+ # Per-request client override (contextvar)
121
+ # ---------------------------------------------------------------------
122
+ #
123
+ # By default, every SDK HTTP call builds an :class:`OuterProductClient`
124
+ # from ``_state`` — one shared bearer token for the whole process. That
125
+ # works for single-user SDK consumers (notebooks, scripts).
126
+ #
127
+ # Multi-tenant SDK consumers (e.g. an MCP server serving many users)
128
+ # need *per-request* credentials. The contextvar below lets a caller
129
+ # enter a scope in which SDK HTTP helpers use a provided pre-built
130
+ # client instead of the global state — without mutating the global
131
+ # state and without changing every SDK class signature.
132
+
133
+ _request_client: ContextVar["OuterProductClient | None"] = ContextVar(
134
+ "outerproduct_request_client", default=None
135
+ )
136
+
137
+ # The async mirror. Sync (`use_client`) and async (`use_aclient`) injection
138
+ # use *separate* contextvars so the two client colors can never be confused:
139
+ # the async transport needs an `AsyncOuterProductClient`, the sync transport an
140
+ # `OuterProductClient`, and a sync helper must never pick up an async client.
141
+ _arequest_client: ContextVar["AsyncOuterProductClient | None"] = ContextVar(
142
+ "outerproduct_arequest_client", default=None
143
+ )
144
+
145
+
146
+ @contextmanager
147
+ def use_client(
148
+ client: "OuterProductClient",
149
+ ) -> Generator[None, None, None]:
150
+ """Use ``client`` for every SDK HTTP call inside this block.
151
+
152
+ Inside the ``with`` block, the SDK's internal client helper yields
153
+ ``client`` rather than building one from ``op._state``. The caller
154
+ owns ``client``'s lifecycle — the SDK will not close it.
155
+
156
+ Designed for service-style consumers (e.g. an MCP server) that need
157
+ to thread per-request credentials through SDK calls without mutating
158
+ global state. Contextvars propagate across ``asyncio.to_thread`` and
159
+ ``asyncio.TaskGroup``, so this works inside async dispatch.
160
+
161
+ Examples
162
+ --------
163
+ .. code-block:: python
164
+
165
+ from outerproduct.client import OuterProductClient
166
+ import outerproduct as op
167
+
168
+ # Mint a per-request bearer (e.g. a short-lived JWT) and use it
169
+ # for every SDK call inside the `with` block.
170
+ with OuterProductClient.from_credentials(base_url, per_request_token) as client:
171
+ with op.use_client(client):
172
+ model = op.ReasoningModel(id="mdl-1", feature_names=["a","b"])
173
+ preds = model.predict(X)
174
+ """
175
+ token = _request_client.set(client)
176
+ try:
177
+ yield
178
+ finally:
179
+ _request_client.reset(token)
180
+
181
+
182
+ @contextmanager
183
+ def use_aclient(
184
+ client: "AsyncOuterProductClient",
185
+ ) -> Generator[None, None, None]:
186
+ """Async mirror of :func:`use_client` — inject an :class:`AsyncOuterProductClient`.
187
+
188
+ Inside this block, the SDK's async HTTP helpers (``_aclient`` /
189
+ ``_aresolve_client``, and therefore every ``a``-prefixed/async SDK call)
190
+ use ``client`` instead of building one from ``op._state``. The caller owns
191
+ ``client``'s lifecycle — the SDK will not ``aclose`` it.
192
+
193
+ This is itself an ordinary (synchronous) context manager — it only sets a
194
+ contextvar and does no I/O — so it can be entered with a plain ``with``
195
+ even though the client it injects is async::
196
+
197
+ async with AsyncOuterProductClient.from_credentials(base, token) as client:
198
+ with op.use_aclient(client):
199
+ await op.reasoning.afit(ds, task=...)
200
+ """
201
+ token = _arequest_client.set(client)
202
+ try:
203
+ yield
204
+ finally:
205
+ _arequest_client.reset(token)
206
+
207
+
208
+ def init(
209
+ api_key: str | None = None,
210
+ *,
211
+ base_url: str | None = None,
212
+ ) -> None:
213
+ """Configure the OuterProduct SDK for the current Python process.
214
+
215
+ Call once near the top of your program. Subsequent operations
216
+ (training, prediction, uploads) read credentials and the API base URL
217
+ from the state recorded here.
218
+
219
+ Examples
220
+ --------
221
+ .. code-block:: python
222
+
223
+ import outerproduct as op
224
+ op.init(api_key="op_live_...")
225
+
226
+ Parameters
227
+ ----------
228
+ api_key : str, optional
229
+ Bearer token used to authenticate against the OuterProduct API.
230
+ If omitted, the ``OUTERPRODUCT_API_KEY`` environment variable is
231
+ consulted at use time.
232
+ base_url : str, optional
233
+ Override for the API base URL. If omitted, the SDK uses the
234
+ production endpoint.
235
+ """
236
+ _state.api_key = api_key
237
+ _state.base_url = base_url
238
+
239
+
240
+ def get_datasets() -> list[Dataset]:
241
+ """List datasets owned by the configured caller.
242
+
243
+ Honors :func:`use_client` — multi-tenant consumers (e.g. an MCP
244
+ server) get per-user authentication via the request-scoped client
245
+ contextvar. Falls back to the process-global API key otherwise.
246
+ """
247
+ from outerproduct.utils import _resolve_client
248
+
249
+ client = _resolve_client()
250
+ response = client.datasets_api.list()
251
+ out: list[Dataset] = []
252
+ for entry in response.datasets:
253
+ dataset = Dataset(entry.connection_config, client, entry.columns)
254
+ dataset._stamp_id(entry.id)
255
+ out.append(dataset)
256
+ return out
257
+
258
+
259
+ def get_dataset(dataset_id: str) -> Dataset:
260
+ """Fetch a single dataset by id; useful for cross-session reuse.
261
+
262
+ The returned Dataset has its ``_id`` and ``connection_config`` already
263
+ stamped, so subsequent fit/predict calls short-circuit the server-side
264
+ create.
265
+
266
+ Honors :func:`use_client` — multi-tenant consumers (e.g. an MCP
267
+ server) get per-user authentication via the request-scoped client
268
+ contextvar. Falls back to the process-global API key otherwise.
269
+ """
270
+ from outerproduct.utils import _resolve_client
271
+
272
+ client = _resolve_client()
273
+ entry = client.datasets_api.get(dataset_id)
274
+ dataset = Dataset(entry.connection_config, client, entry.columns)
275
+ dataset._stamp_id(entry.id)
276
+ return dataset
277
+
278
+
279
+ def get_models() -> list["Model"]:
280
+ """List models owned by the configured caller.
281
+
282
+ Rows tagged ``model_type='reasoning'`` come back as
283
+ :class:`~outerproduct.model.ReasoningModel`; trainer rows come back
284
+ as :class:`~outerproduct.model.Model`. Both expose ``id`` /
285
+ ``dataset_id`` already stamped from the wire response; richer
286
+ attributes (``schema``, ``feature_names``) lazy-fetch on first use.
287
+ """
288
+ from outerproduct.model import Model
289
+ from outerproduct.utils import _client
290
+
291
+ with _client() as client:
292
+ response = client.models_api.list()
293
+ return [Model._from_response(record) for record in response.models]
294
+
295
+
296
+ def get_model(model_id: str) -> "Model":
297
+ """Fetch a single model by id; dispatches to Model / ReasoningModel
298
+ based on the row's ``model_type``."""
299
+ from outerproduct.model import Model
300
+ from outerproduct.utils import _client
301
+
302
+ with _client() as client:
303
+ record = client.models_api.get(model_id)
304
+ return Model._from_response(record)
305
+
306
+
307
+ __all__ = [
308
+ # Bootstrap
309
+ "init",
310
+ "use_client",
311
+ "use_aclient",
312
+ "get_dataset",
313
+ "get_datasets",
314
+ "get_model",
315
+ "get_models",
316
+ "__version__",
317
+ # Dataset
318
+ "Dataset",
319
+ "LocalDataset",
320
+ "DatasetColumn",
321
+ "Datatype",
322
+ "col",
323
+ "Expr",
324
+ # Jobs
325
+ "Job",
326
+ "JobFailed",
327
+ "JobNotReady",
328
+ "JobResult",
329
+ "ReasoningFitJob",
330
+ "TrainerJob",
331
+ # Model
332
+ "Model",
333
+ "DiffModel",
334
+ "ReasoningModel",
335
+ "SequenceReasoningModel",
336
+ # Reasoning (submodule, accessed as op.reasoning.*)
337
+ "reasoning",
338
+ "Reasoning",
339
+ "ScenarioResult",
340
+ "QueryResult",
341
+ "Counterfactual",
342
+ "Change",
343
+ # Trainer
344
+ "Trainer",
345
+ "Metric",
346
+ # Task config
347
+ "TaskConfig",
348
+ "TaskKind",
349
+ "RegressionConfig",
350
+ "BinclassConfig",
351
+ "MulticlassConfig",
352
+ "ForecastingConfig",
353
+ "SequenceConfig",
354
+ # Connector
355
+ "Connector",
356
+ "ConnectorType",
357
+ "S3Connector",
358
+ "SnowflakeConnector",
359
+ "DatabricksConnector",
360
+ # Agentic feature sourcing (submodule, accessed as op.agentic.*)
361
+ "agentic",
362
+ ]