outerproduct 0.3.0__tar.gz → 0.3.2__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.
Files changed (27) hide show
  1. outerproduct-0.3.2/.gitignore +5 -0
  2. {outerproduct-0.3.0 → outerproduct-0.3.2}/PKG-INFO +3 -5
  3. {outerproduct-0.3.0 → outerproduct-0.3.2}/pyproject.toml +13 -21
  4. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/__init__.py +30 -21
  5. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/agentic/documents.py +5 -5
  6. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/client/apis.py +183 -20
  7. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/dataset/__init__.py +24 -9
  8. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/job.py +1 -2
  9. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/model.py +12 -7
  10. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/reasoning/__init__.py +18 -12
  11. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/reasoning/pattern_tracker.py +2 -2
  12. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/trainer.py +13 -12
  13. outerproduct-0.3.0/.gitignore +0 -16
  14. {outerproduct-0.3.0 → outerproduct-0.3.2}/LICENSE +0 -0
  15. {outerproduct-0.3.0 → outerproduct-0.3.2}/README.md +0 -0
  16. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/agentic/__init__.py +0 -0
  17. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/client/__init__.py +0 -0
  18. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/client/_http.py +0 -0
  19. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/client/client.py +0 -0
  20. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/client/exceptions.py +0 -0
  21. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/connector.py +0 -0
  22. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/dataset/datatypes.py +0 -0
  23. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/expr.py +0 -0
  24. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/id.py +0 -0
  25. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/job_result.py +0 -0
  26. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/py.typed +0 -0
  27. {outerproduct-0.3.0 → outerproduct-0.3.2}/src/outerproduct/utils.py +0 -0
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ uv.lock
4
+ .pytest_cache/
5
+ .ruff_cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: outerproduct
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -23,17 +23,15 @@ Requires-Python: >=3.12
23
23
  Requires-Dist: httpx~=0.27
24
24
  Requires-Dist: narwhals>=1.13
25
25
  Requires-Dist: numpy~=1.23
26
- Requires-Dist: outerproduct-http-types
26
+ Requires-Dist: outerproduct-http-types>=0.7.3
27
27
  Requires-Dist: pandas~=2.0
28
28
  Requires-Dist: polars<=1.41.0
29
+ Requires-Dist: pyarrow>=14.0
29
30
  Requires-Dist: pydantic<3,>=2.0
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: anyio; extra == 'dev'
32
- Requires-Dist: pyarrow>=14.0; extra == 'dev'
33
33
  Requires-Dist: pytest; extra == 'dev'
34
34
  Requires-Dist: respx; extra == 'dev'
35
- Provides-Extra: parquet
36
- Requires-Dist: pyarrow>=14.0; extra == 'parquet'
37
35
  Description-Content-Type: text/markdown
38
36
 
39
37
  # outerproduct-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "outerproduct"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Python SDK for the OuterProduct API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -27,9 +27,12 @@ dependencies = [
27
27
  "httpx~=0.27",
28
28
  "narwhals>=1.13",
29
29
  "numpy~=1.23",
30
- "outerproduct-http-types",
30
+ # Joint Python package releases raise this floor to the http-types version
31
+ # published with the SDK; in-repo builds resolve it from the path source below.
32
+ "outerproduct-http-types>=0.7.3",
31
33
  "pandas~=2.0",
32
34
  "polars<=1.41.0",
35
+ "pyarrow>=14.0",
33
36
  "pydantic>=2.0,<3",
34
37
  ]
35
38
 
@@ -38,14 +41,12 @@ Documentation = "https://docs.outerproduct.com"
38
41
  Homepage = "https://outerproduct.com"
39
42
 
40
43
  [project.optional-dependencies]
41
- dev = ["anyio", "pytest", "respx", "pyarrow>=14.0"]
42
- parquet = ["pyarrow>=14.0"]
44
+ dev = ["anyio", "pytest", "respx"]
43
45
 
44
46
  [dependency-groups]
45
47
  dev = [
46
48
  "anyio>=4.0",
47
49
  "prek>=0.3.11",
48
- "pyarrow>=14.0",
49
50
  "pytest>=9.0.3",
50
51
  "respx>=0.23.1",
51
52
  "ruff>=0.15.12",
@@ -69,7 +70,7 @@ include = [
69
70
  ]
70
71
 
71
72
  [tool.hatch.envs.hatch-test]
72
- extra-dependencies = ["anyio", "respx", "pyarrow>=14.0"]
73
+ extra-dependencies = ["anyio", "respx"]
73
74
 
74
75
  [[tool.hatch.envs.hatch-test.matrix]]
75
76
  python = ["3.12", "3.13", "3.14"]
@@ -111,18 +112,9 @@ exclude = [".agents/**", ".ruff_cache/**", ".venv/**"]
111
112
  [tool.ty.src]
112
113
  include = ["src"]
113
114
 
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 }
115
+ # In the monorepo the wire-types package is a sibling under packages/. Resolve
116
+ # it from source so the SDK and its consumers share one copy of http-types.
117
+ # This [tool.uv.sources] override is uv-only and stripped at build time, so the
118
+ # published wheel keeps the PyPI lower bound above.
119
+ [tool.uv.sources]
120
+ outerproduct-http-types = { path = "../http-types" }
@@ -34,8 +34,8 @@ Quickstart
34
34
 
35
35
  op.init(api_key="...")
36
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)
37
+ reasoning_model = op.reasoning.fit(dataset, task=op.Binclass(label_column="churn"))
38
+ test_dataset = op.LocalDataset.from_pandas(X_new).upload()
39
39
  predictions = reasoning_model.predict(test_dataset)
40
40
  reasoning = reasoning_model.explain(test_dataset)
41
41
  """
@@ -49,17 +49,19 @@ from importlib.metadata import version as _pkg_version
49
49
  from typing import TYPE_CHECKING, Generator
50
50
 
51
51
  from outerproduct_http_types import (
52
- BinclassConfig,
52
+ Binclass,
53
53
  ConnectorType,
54
- ForecastingConfig,
55
- MulticlassConfig,
56
- RegressionConfig,
57
- SequenceConfig,
58
- TaskConfig,
54
+ Forecasting,
55
+ Multiclass,
56
+ Regression,
57
+ SequenceBinclass,
58
+ SequenceMulticlass,
59
+ SequenceRegression,
60
+ Task,
59
61
  TaskKind,
60
62
  )
61
63
 
62
- from outerproduct import agentic
64
+ from outerproduct import agentic, reasoning
63
65
  from outerproduct.connector import (
64
66
  Connector,
65
67
  DatabricksConnector,
@@ -73,7 +75,13 @@ from outerproduct.dataset import (
73
75
  LocalDataset,
74
76
  )
75
77
  from outerproduct.expr import Expr, col
76
- from outerproduct.job import Job, JobFailed, JobNotReady, ReasoningFitJob, TrainerJob
78
+ from outerproduct.job import (
79
+ Job,
80
+ JobFailed,
81
+ JobNotReady,
82
+ ReasoningFitJob,
83
+ TrainerJob,
84
+ )
77
85
  from outerproduct.job_result import JobResult
78
86
  from outerproduct.model import (
79
87
  DiffModel,
@@ -81,15 +89,14 @@ from outerproduct.model import (
81
89
  ReasoningModel,
82
90
  SequenceReasoningModel,
83
91
  )
84
- from outerproduct import reasoning
85
92
  from outerproduct.reasoning import (
86
93
  Change,
87
94
  Counterfactual,
88
95
  QueryResult,
89
96
  Reasoning,
90
97
  ScenarioResult,
98
+ pattern_tracker, # noqa: F401 — makes op.reasoning.pattern_tracker accessible
91
99
  )
92
- from outerproduct.reasoning import pattern_tracker # noqa: F401 — makes op.reasoning.pattern_tracker accessible
93
100
  from outerproduct.trainer import (
94
101
  Metric,
95
102
  Trainer,
@@ -251,7 +258,7 @@ def get_datasets() -> list[Dataset]:
251
258
  out: list[Dataset] = []
252
259
  for entry in response.datasets:
253
260
  dataset = Dataset(entry.connection_config, client, entry.columns)
254
- dataset._stamp_id(entry.id)
261
+ dataset._set_id(entry.id)
255
262
  out.append(dataset)
256
263
  return out
257
264
 
@@ -272,7 +279,7 @@ def get_dataset(dataset_id: str) -> Dataset:
272
279
  client = _resolve_client()
273
280
  entry = client.datasets_api.get(dataset_id)
274
281
  dataset = Dataset(entry.connection_config, client, entry.columns)
275
- dataset._stamp_id(entry.id)
282
+ dataset._set_id(entry.id)
276
283
  return dataset
277
284
 
278
285
 
@@ -343,14 +350,16 @@ __all__ = [
343
350
  # Trainer
344
351
  "Trainer",
345
352
  "Metric",
346
- # Task config
347
- "TaskConfig",
353
+ # Task
354
+ "Task",
348
355
  "TaskKind",
349
- "RegressionConfig",
350
- "BinclassConfig",
351
- "MulticlassConfig",
352
- "ForecastingConfig",
353
- "SequenceConfig",
356
+ "Regression",
357
+ "Binclass",
358
+ "Multiclass",
359
+ "Forecasting",
360
+ "SequenceRegression",
361
+ "SequenceBinclass",
362
+ "SequenceMulticlass",
354
363
  # Connector
355
364
  "Connector",
356
365
  "ConnectorType",
@@ -339,7 +339,7 @@ class DocumentDataset(Dataset):
339
339
  DatasetColumn(name=q.id, datatype=_ANSWER_TYPE_TO_DATATYPE[q.answer_type])
340
340
  for q in schema.questions
341
341
  ]
342
- self._id = dataset_id # ty: ignore[invalid-assignment] # TODO: deferred DocumentDataset port
342
+ self._id = dataset_id # TODO: deferred DocumentDataset port
343
343
  self._connection_config = None
344
344
  self._remote_only = True
345
345
  self._schema = schema
@@ -486,7 +486,7 @@ def induce_schema(
486
486
  submission = op.agentic_documents_api.induce_schema(
487
487
  InduceSchemaRequest(documents=refs, use_case=use_case, skill=skill)
488
488
  )
489
- op.wait_for(submission.job_id)
489
+ op.wait_for(str(submission.job_id))
490
490
  result = op.agentic_documents_api.get_schema(submission.schema_id)
491
491
  return _schema_from_payload(result.schema_)
492
492
 
@@ -521,12 +521,12 @@ def tabularize(
521
521
  }
522
522
  )
523
523
  )
524
- op.wait_for(submission.job_id)
525
- result = op.agentic_documents_api.get_table(submission.dataset_id)
524
+ op.wait_for(str(submission.job_id))
525
+ result = op.agentic_documents_api.get_table(str(submission.dataset_id))
526
526
  return DocumentDataset(
527
527
  schema=_schema_from_payload(result.schema_),
528
528
  document_ids=list(result.document_ids),
529
- dataset_id=result.dataset_id,
529
+ dataset_id=str(result.dataset_id),
530
530
  label_column=label_column,
531
531
  )
532
532
 
@@ -9,8 +9,10 @@ ergonomic method names without duplicating any type machinery.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from typing import TYPE_CHECKING, cast
12
+ from typing import TYPE_CHECKING, Any, cast
13
13
 
14
+ import httpx
15
+ from pydantic import TypeAdapter
14
16
  from outerproduct_http_types import (
15
17
  CreateDatasetRequest,
16
18
  CreateDatasetResponse,
@@ -24,6 +26,8 @@ from outerproduct_http_types import (
24
26
  GlobalDriversResponse,
25
27
  InduceSchemaJobResponse,
26
28
  InduceSchemaRequest,
29
+ JobResultResponse,
30
+ JobResponse,
27
31
  ListDatasetEntry,
28
32
  ListDatasetsResponse,
29
33
  ListModelsResponse,
@@ -86,6 +90,91 @@ if TYPE_CHECKING:
86
90
  )
87
91
 
88
92
 
93
+ INFERENCE_RESULT_DOWNLOAD_TIMEOUT_SECONDS = 60.0
94
+
95
+
96
+ def inference_result_download_url(result: JobResultResponse) -> str:
97
+ data = result.result_data or {}
98
+ url = data.get("presigned_url")
99
+ if not isinstance(url, str) or not url:
100
+ raise RuntimeError(
101
+ f"Inference job {result.job_id} result did not include a presigned_url"
102
+ )
103
+ return url
104
+
105
+
106
+ def parse_inference_payload(
107
+ job_id: str, payload: object, response_type: Any
108
+ ) -> Any:
109
+ if not isinstance(payload, dict):
110
+ raise RuntimeError(
111
+ f"Inference job {job_id} result payload was not a JSON object"
112
+ )
113
+ return TypeAdapter(response_type).validate_python(payload)
114
+
115
+
116
+ def assert_inference_payload_object(job_id: str, payload: object) -> dict[str, Any]:
117
+ if not isinstance(payload, dict):
118
+ raise RuntimeError(
119
+ f"Inference job {job_id} result payload was not a JSON object"
120
+ )
121
+ return cast(dict[str, Any], payload)
122
+
123
+
124
+ def download_inference_result(result: JobResultResponse) -> dict[str, Any]:
125
+ response = httpx.get(
126
+ inference_result_download_url(result),
127
+ timeout=INFERENCE_RESULT_DOWNLOAD_TIMEOUT_SECONDS,
128
+ )
129
+ response.raise_for_status()
130
+ return assert_inference_payload_object(str(result.job_id), response.json())
131
+
132
+
133
+ async def async_download_inference_result(
134
+ result: JobResultResponse,
135
+ ) -> dict[str, Any]:
136
+ async with httpx.AsyncClient(
137
+ timeout=INFERENCE_RESULT_DOWNLOAD_TIMEOUT_SECONDS
138
+ ) as client:
139
+ response = await client.get(inference_result_download_url(result))
140
+ response.raise_for_status()
141
+ return assert_inference_payload_object(str(result.job_id), response.json())
142
+
143
+
144
+ def submit_inference_job(
145
+ client: OuterProductClient,
146
+ endpoint: Any,
147
+ request: Any,
148
+ *,
149
+ model_id: str,
150
+ response_type: Any,
151
+ ) -> Any:
152
+ response = cast(JobResponse, client._post(endpoint, request, model_id=model_id))
153
+ job_id = str(response.job_id)
154
+ client.wait_for(job_id)
155
+ result = client.results(job_id)
156
+ payload = download_inference_result(result)
157
+ return TypeAdapter(response_type).validate_python(payload)
158
+
159
+
160
+ async def async_submit_inference_job(
161
+ client: AsyncOuterProductClient,
162
+ endpoint: Any,
163
+ request: Any,
164
+ *,
165
+ model_id: str,
166
+ response_type: Any,
167
+ ) -> Any:
168
+ response = cast(
169
+ JobResponse, await client._post(endpoint, request, model_id=model_id)
170
+ )
171
+ job_id = str(response.job_id)
172
+ await client.wait_for(job_id)
173
+ result = await client.results(job_id)
174
+ payload = await async_download_inference_result(result)
175
+ return TypeAdapter(response_type).validate_python(payload)
176
+
177
+
89
178
  class UploadsAPI:
90
179
  """``POST /v1/uploads`` — request a presigned upload URL."""
91
180
 
@@ -178,7 +267,12 @@ class ModelsAPI:
178
267
 
179
268
 
180
269
  class InferenceAPI:
181
- """Synchronous inference on ``/v1/models/{model_id}/...``."""
270
+ """Inference on ``/v1/models/{model_id}/...``.
271
+
272
+ The API runs inference as a job, but this SDK surface preserves the
273
+ synchronous inference contract: each method waits for the job and returns
274
+ the typed inference response.
275
+ """
182
276
 
183
277
  def __init__(self, client: OuterProductClient) -> None:
184
278
  self._client = client
@@ -186,11 +280,15 @@ class InferenceAPI:
186
280
  def predict(
187
281
  self, model_id: str, request: PredictRequest
188
282
  ) -> PredictResponse | SequencePredictResponse:
189
- # cast: the PREDICT endpoint's response_type is this union, but ty
190
- # can't bind the generic RespT through it; the cast keeps it ty-clean.
191
283
  return cast(
192
284
  "PredictResponse | SequencePredictResponse",
193
- self._client._post(PREDICT, request, model_id=model_id),
285
+ submit_inference_job(
286
+ self._client,
287
+ PREDICT,
288
+ request,
289
+ model_id=model_id,
290
+ response_type=SequencePredictResponse | PredictResponse,
291
+ ),
194
292
  )
195
293
 
196
294
  def explain(
@@ -198,23 +296,54 @@ class InferenceAPI:
198
296
  ) -> ExplainResponse | SequenceExplainResponse:
199
297
  return cast(
200
298
  "ExplainResponse | SequenceExplainResponse",
201
- self._client._post(EXPLAIN, request, model_id=model_id),
299
+ submit_inference_job(
300
+ self._client,
301
+ EXPLAIN,
302
+ request,
303
+ model_id=model_id,
304
+ response_type=SequenceExplainResponse | ExplainResponse,
305
+ ),
202
306
  )
203
307
 
204
308
  def predict_and_explain(
205
309
  self, model_id: str, request: PredictAndExplainRequest
206
310
  ) -> PredictAndExplainResponse:
207
- return self._client._post(PREDICT_AND_EXPLAIN, request, model_id=model_id)
311
+ return cast(
312
+ "PredictAndExplainResponse",
313
+ submit_inference_job(
314
+ self._client,
315
+ PREDICT_AND_EXPLAIN,
316
+ request,
317
+ model_id=model_id,
318
+ response_type=PredictAndExplainResponse,
319
+ ),
320
+ )
208
321
 
209
322
  def get_global_drivers(
210
323
  self, model_id: str, request: GlobalDriversRequest | None = None
211
324
  ) -> GlobalDriversResponse:
212
- return self._client._post(
213
- GLOBAL_DRIVERS, request or GlobalDriversRequest(), model_id=model_id
325
+ return cast(
326
+ "GlobalDriversResponse",
327
+ submit_inference_job(
328
+ self._client,
329
+ GLOBAL_DRIVERS,
330
+ request or GlobalDriversRequest(),
331
+ model_id=model_id,
332
+ response_type=GlobalDriversResponse,
333
+ ),
214
334
  )
215
335
 
216
336
  def scenario(self, model_id: str, request: ScenarioRequest) -> ScenarioResponse:
217
- return self._client._post(SCENARIO, request, model_id=model_id)
337
+ return cast(
338
+ "ScenarioResponse",
339
+ submit_inference_job(
340
+ self._client,
341
+ SCENARIO,
342
+ request,
343
+ model_id=model_id,
344
+ response_type=ScenarioResponse,
345
+ ),
346
+ )
218
347
 
219
348
 
220
349
  class PatternTrackerAPI:
@@ -369,7 +498,7 @@ class AsyncModelsAPI:
369
498
 
370
499
 
371
500
  class AsyncInferenceAPI:
372
- """Synchronous-endpoint inference on ``/v1/models/{model_id}/...`` (async client)."""
501
+ """Inference on ``/v1/models/{model_id}/...`` for the async client."""
373
502
 
374
503
  def __init__(self, client: AsyncOuterProductClient) -> None:
375
504
  self._client = client
@@ -377,12 +506,15 @@ class AsyncInferenceAPI:
377
506
  async def predict(
378
507
  self, model_id: str, request: PredictRequest
379
508
  ) -> PredictResponse | SequencePredictResponse:
380
- # cast: the PREDICT endpoint's response_type is this union, but ty
381
- # can't bind the generic RespT through it (same limitation the sync
382
- # InferenceAPI.predict hits); the cast keeps the async twin ty-clean.
383
509
  return cast(
384
510
  "PredictResponse | SequencePredictResponse",
385
- await self._client._post(PREDICT, request, model_id=model_id),
511
+ await async_submit_inference_job(
512
+ self._client,
513
+ PREDICT,
514
+ request,
515
+ model_id=model_id,
516
+ response_type=SequencePredictResponse | PredictResponse,
517
+ ),
386
518
  )
387
519
 
388
520
  async def explain(
@@ -390,25 +522,56 @@ class AsyncInferenceAPI:
390
522
  ) -> ExplainResponse | SequenceExplainResponse:
391
523
  return cast(
392
524
  "ExplainResponse | SequenceExplainResponse",
393
- await self._client._post(EXPLAIN, request, model_id=model_id),
525
+ await async_submit_inference_job(
526
+ self._client,
527
+ EXPLAIN,
528
+ request,
529
+ model_id=model_id,
530
+ response_type=SequenceExplainResponse | ExplainResponse,
531
+ ),
394
532
  )
395
533
 
396
534
  async def predict_and_explain(
397
535
  self, model_id: str, request: PredictAndExplainRequest
398
536
  ) -> PredictAndExplainResponse:
399
- return await self._client._post(PREDICT_AND_EXPLAIN, request, model_id=model_id)
537
+ return cast(
538
+ "PredictAndExplainResponse",
539
+ await async_submit_inference_job(
540
+ self._client,
541
+ PREDICT_AND_EXPLAIN,
542
+ request,
543
+ model_id=model_id,
544
+ response_type=PredictAndExplainResponse,
545
+ ),
546
+ )
400
547
 
401
548
  async def get_global_drivers(
402
549
  self, model_id: str, request: GlobalDriversRequest | None = None
403
550
  ) -> GlobalDriversResponse:
404
- return await self._client._post(
405
- GLOBAL_DRIVERS, request or GlobalDriversRequest(), model_id=model_id
551
+ return cast(
552
+ "GlobalDriversResponse",
553
+ await async_submit_inference_job(
554
+ self._client,
555
+ GLOBAL_DRIVERS,
556
+ request or GlobalDriversRequest(),
557
+ model_id=model_id,
558
+ response_type=GlobalDriversResponse,
559
+ ),
406
560
  )
407
561
 
408
562
  async def scenario(
409
563
  self, model_id: str, request: ScenarioRequest
410
564
  ) -> ScenarioResponse:
411
- return await self._client._post(SCENARIO, request, model_id=model_id)
565
+ return cast(
566
+ "ScenarioResponse",
567
+ await async_submit_inference_job(
568
+ self._client,
569
+ SCENARIO,
570
+ request,
571
+ model_id=model_id,
572
+ response_type=ScenarioResponse,
573
+ ),
574
+ )
412
575
 
413
576
 
414
577
  class AsyncPatternTrackerAPI:
@@ -24,7 +24,6 @@ from typing import TYPE_CHECKING, Any, cast
24
24
  from outerproduct_http_types import (
25
25
  ConnectionConfig,
26
26
  DatasetColumn,
27
- FileUploadConnectionConfig,
28
27
  )
29
28
  from outerproduct_http_types import Dataset as CreateDatasetRequestBody
30
29
  from outerproduct_http_types import FilterExpr as WireFilterExpr
@@ -41,6 +40,7 @@ from outerproduct.dataset.datatypes import Datatype, coerce, from_narwhals_dtype
41
40
  if TYPE_CHECKING:
42
41
  import numpy as np
43
42
  import pandas as pd
43
+ import polars as pl
44
44
  from narwhals.stable.v1 import DataFrame as Frame
45
45
  from narwhals.stable.v1.typing import IntoDataFrame
46
46
 
@@ -82,7 +82,7 @@ class LocalDataset(BaseModel):
82
82
  return LocalDataset._from_data(df)
83
83
 
84
84
  @staticmethod
85
- def from_polars(df: Any) -> LocalDataset:
85
+ def from_polars(df: pl.DataFrame) -> LocalDataset:
86
86
  return LocalDataset._from_data(df)
87
87
 
88
88
  @staticmethod
@@ -205,7 +205,15 @@ class LocalDataset(BaseModel):
205
205
  # ------------------------------------------------------------------
206
206
 
207
207
  def upload(self, client: OuterProductClient | None = None) -> Dataset:
208
- """Serialize the rows to Parquet, PUT them, and return a :class:`Dataset`.
208
+ """Serialize the rows to Parquet, PUT them, register the dataset, and
209
+ return a fully-registered :class:`Dataset`.
210
+
211
+ Three server round-trips: ``POST /v1/uploads`` mints the upload's
212
+ connection-config row and presigned PUT, the bytes are PUT to S3, then
213
+ ``POST /v1/datasets`` registers the dataset over that config — which
214
+ HEAD-verifies the upload landed and mints the Postgres dataset id. The
215
+ returned handle is registered (``_id`` set), so downstream fit/predict
216
+ calls reuse it without re-registering.
209
217
 
210
218
  ``client`` defaults to the one configured via :func:`outerproduct.init`.
211
219
  This is the real implementation (the underlying client is synchronous);
@@ -225,16 +233,23 @@ class LocalDataset(BaseModel):
225
233
  f"upload cap is {MAX_UPLOAD_BYTES / 1e9:.0f} GB. For larger datasets, "
226
234
  "register an external connector (S3, Snowflake, Databricks) instead."
227
235
  )
228
- upload = client.uploads_api.create(CreateUploadRequest(file_format="parquet"))
236
+ upload_response = client.uploads_api.create(
237
+ CreateUploadRequest(file_format="parquet")
238
+ )
229
239
  with httpx.Client() as http:
230
240
  put_resp = http.put(
231
- upload.upload_url,
241
+ upload_response.upload_url,
232
242
  content=body,
233
- headers={"Content-Type": upload.content_type},
243
+ headers={"Content-Type": upload_response.content_type},
234
244
  )
235
245
  put_resp.raise_for_status()
236
- config = FileUploadConnectionConfig(uri=upload.data_uri, file_format="parquet")
237
- return Dataset(config, client, self.columns)
246
+ # The server minted the file_upload_connection_config row and returned
247
+ # it as ``connection_config`` (the upload URI is no longer the id).
248
+ # Register the dataset now so the Postgres-generated id is assigned and
249
+ # the upload is verified server-side before returning.
250
+ ds = Dataset(upload_response.connection_config, client, self.columns)
251
+ ds._ensure_registered()
252
+ return ds
238
253
 
239
254
  async def aupload(self, client: OuterProductClient | None = None) -> Dataset:
240
255
  """Async :meth:`upload`.
@@ -297,7 +312,7 @@ class Dataset(BaseModel):
297
312
  connection_config, _resolve_client(), columns if columns is not None else []
298
313
  )
299
314
 
300
- def _stamp_id(self, dataset_id: ID) -> None:
315
+ def _set_id(self, dataset_id: ID) -> None:
301
316
  """Cache the server-assigned id from a fit/predict response."""
302
317
  self._id = dataset_id
303
318
 
@@ -30,7 +30,6 @@ from outerproduct.utils import _client
30
30
  if TYPE_CHECKING:
31
31
  from outerproduct.model import Model, ReasoningModel, SequenceReasoningModel
32
32
 
33
-
34
33
  class JobFailed(RuntimeError):
35
34
  """Raised by :meth:`Job.results` when the server reports status='failed'."""
36
35
 
@@ -90,7 +89,7 @@ class Job:
90
89
  if body.status == "failed":
91
90
  raise JobFailed(body.error_message or f"job {self._job_id} failed")
92
91
  return JobResult(
93
- job_id=body.job_id,
92
+ job_id=str(body.job_id),
94
93
  result_uri=body.result_uri,
95
94
  completed_at=body.completed_at,
96
95
  raw=dict(body.result_data or {}),
@@ -15,8 +15,8 @@ End-to-end shape:
15
15
 
16
16
  import outerproduct as op
17
17
  dataset = op.LocalDataset.from_csv("data.csv").upload()
18
- reasoning_model = op.reasoning.fit(dataset, label_column="churn")
19
- test_dataset = op.Dataset(test_df)
18
+ reasoning_model = op.reasoning.fit(dataset, task=op.Binclass(label_column="churn"))
19
+ test_dataset = op.LocalDataset.from_pandas(test_df).upload()
20
20
  predictions = reasoning_model.predict(test_dataset)
21
21
  reasoning = reasoning_model.explain(test_dataset)
22
22
  """
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
31
31
  import numpy as np
32
32
  from outerproduct_http_types import Dataset as WireDataset
33
33
  from outerproduct_http_types import ExplainResponse
34
+ from outerproduct_http_types import SequenceExplainResponse
34
35
 
35
36
  from outerproduct.dataset import Dataset, DatasetColumn
36
37
  from outerproduct.reasoning import Reasoning, ScenarioResult
@@ -209,9 +210,9 @@ class Model:
209
210
  Parameters
210
211
  ----------
211
212
  dataset : :class:`~outerproduct.dataset.Dataset`
212
- Input features. Wrap a local ``pandas.DataFrame`` /
213
- ``numpy.ndarray`` / list with :class:`~outerproduct.dataset.Dataset`
214
- first — the SDK no longer accepts raw arrays.
213
+ Input features. Build one with
214
+ ``op.LocalDataset.from_*(...).upload()`` (or a connector) — the
215
+ SDK does not accept a raw DataFrame / ndarray.
215
216
 
216
217
  Returns
217
218
  -------
@@ -574,7 +575,8 @@ class SequenceReasoningModel(Model):
574
575
  """A sequence-task model: ``predict`` + per-entity ``explain`` only.
575
576
 
576
577
  Returned by :func:`~outerproduct.reasoning.fit` when the task is a
577
- :class:`~outerproduct_http_types.SequenceConfig`. Inherits ``predict``
578
+ sequence variant (:class:`~outerproduct_http_types.SequenceBinclass` /
579
+ :class:`~outerproduct_http_types.SequenceMulticlass`). Inherits ``predict``
578
580
  (one prediction per entity). ``explain`` returns the per-entity attribution
579
581
  tensor. The flat-reasoning surfaces (``scenario``, ``get_global_drivers``,
580
582
  ``predict_and_explain``, and ``PatternTracker``) do not apply to sequence
@@ -597,7 +599,10 @@ class SequenceReasoningModel(Model):
597
599
 
598
600
  spec = _dataset_spec(dataset, "SequenceReasoningModel.explain")
599
601
  with _client() as c:
600
- resp = c.inference_api.explain(self._id, ExplainRequest(dataset=spec))
602
+ resp = cast(
603
+ "SequenceExplainResponse",
604
+ c.inference_api.explain(self._id, ExplainRequest(dataset=spec)),
605
+ )
601
606
  return Reasoning(feature_names=[], attributions=np.asarray(resp.attributions))
602
607
 
603
608
  @staticmethod
@@ -19,8 +19,8 @@ Examples
19
19
  import outerproduct as op
20
20
 
21
21
  dataset = op.LocalDataset.from_csv("data.csv").upload()
22
- rm = op.reasoning.fit(dataset, task=op.BinclassConfig(label_column="churn"))
23
- reasoning = rm.explain(op.Dataset(X))
22
+ rm = op.reasoning.fit(dataset, task=op.Binclass(label_column="churn"))
23
+ reasoning = rm.explain(op.LocalDataset.from_pandas(X).upload())
24
24
  reasoning.feature_names[:3]
25
25
  """
26
26
 
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, Any
31
31
 
32
32
  if TYPE_CHECKING:
33
33
  import numpy as np
34
- from outerproduct_http_types import TaskConfig
34
+ from outerproduct_http_types import Task
35
35
 
36
36
  from outerproduct.dataset import Dataset
37
37
  from outerproduct.job import ReasoningFitJob
@@ -155,7 +155,7 @@ class ScenarioResult:
155
155
  def fit(
156
156
  dataset: Dataset,
157
157
  *,
158
- task: TaskConfig | None = None,
158
+ task: Task | None = None,
159
159
  teacher: Model | Predictor | None = None,
160
160
  model_types: list[str] | None = None,
161
161
  metric: Metric | list[Metric] | None = None,
@@ -188,7 +188,7 @@ def fit(
188
188
  dataset = op.LocalDataset.from_csv("customers.csv").upload()
189
189
 
190
190
  # Supervised: train against labels.
191
- rm = op.reasoning.fit(dataset, task=op.BinclassConfig(label_column="churn")).wait()
191
+ rm = op.reasoning.fit(dataset, task=op.Binclass(label_column="churn")).wait()
192
192
 
193
193
  # Distill an external prediction API.
194
194
  teacher = op.model.Predictor(
@@ -199,7 +199,7 @@ def fit(
199
199
 
200
200
  # Distill an existing OuterProduct-trained model.
201
201
  trained = op.Trainer.configure(
202
- dataset, task=op.BinclassConfig(label_column="churn")
202
+ dataset, task=op.Binclass(label_column="churn")
203
203
  ).run().wait()
204
204
  rm_wrapped = op.reasoning.fit(dataset, teacher=trained).wait()
205
205
 
@@ -207,9 +207,9 @@ def fit(
207
207
  ----------
208
208
  dataset : :class:`~outerproduct.dataset.Dataset`
209
209
  Training data.
210
- task : :class:`~outerproduct_http_types.TaskConfig`, optional
210
+ task : :class:`~outerproduct_http_types.Task`, optional
211
211
  The learning task and its target columns (e.g.
212
- ``op.RegressionConfig(label_column=...)``). Required for supervised
212
+ ``op.Regression(label_column=...)``). Required for supervised
213
213
  fits; ignored when ``teacher`` is set (the teacher provides the
214
214
  training target).
215
215
  teacher : :class:`~outerproduct.model.Model` or :class:`~outerproduct.model.Predictor`, optional
@@ -288,13 +288,19 @@ def fit(
288
288
 
289
289
  with _client() as client:
290
290
  resp = client.reasoning_api.fit(request)
291
- dataset._stamp_id(resp.dataset_id)
291
+ dataset._set_id(resp.dataset_id)
292
292
 
293
- from outerproduct_http_types import SequenceConfig
293
+ from outerproduct_http_types import (
294
+ SequenceBinclass,
295
+ SequenceMulticlass,
296
+ SequenceRegression,
297
+ )
294
298
 
295
299
  return ReasoningFitJob(
296
- resp.job_id,
300
+ str(resp.job_id),
297
301
  feature_names=ds_feature_names,
298
302
  label_column=label_column,
299
- is_sequence=isinstance(task, SequenceConfig),
303
+ is_sequence=isinstance(
304
+ task, (SequenceRegression, SequenceBinclass, SequenceMulticlass)
305
+ ),
300
306
  )
@@ -10,7 +10,7 @@ Quickstart
10
10
 
11
11
  op.init(api_key="...")
12
12
  ds = op.LocalDataset.from_csv("data.csv").upload()
13
- rm = op.reasoning.fit(ds, task=op.BinclassConfig(label_column="churn"))
13
+ rm = op.reasoning.fit(ds, task=op.Binclass(label_column="churn"))
14
14
  pt = op.reasoning.pattern_tracker.fit(rm, ds, target_range=(0.5, None))
15
15
 
16
16
  pt.patterns # tuple[FilterPattern, ...]
@@ -254,7 +254,7 @@ def fit(
254
254
  resp = c.pattern_tracker_api.create_fit(reasoning_model.id, request)
255
255
 
256
256
  return PatternTrackerJob(
257
- job_id=resp.job_id,
257
+ job_id=str(resp.job_id),
258
258
  model_id=reasoning_model.id,
259
259
  tracker_id=resp.tracker_id,
260
260
  )
@@ -33,12 +33,12 @@ Examples
33
33
  dataset = op.LocalDataset.from_csv("data.csv").upload()
34
34
  trainer = op.Trainer.configure(
35
35
  dataset,
36
- task=op.BinclassConfig(label_column="churn"),
36
+ task=op.Binclass(label_column="churn"),
37
37
  model_types=["tabm", "xgboost"],
38
38
  )
39
39
  job = trainer.run(strategy="optuna")
40
40
  model = job.wait()
41
- predictions = model.predict(op.Dataset(X))
41
+ predictions = model.predict(op.LocalDataset.from_pandas(X).upload())
42
42
  """
43
43
 
44
44
  from __future__ import annotations
@@ -48,7 +48,7 @@ from dataclasses import dataclass
48
48
  from typing import TYPE_CHECKING, Any
49
49
 
50
50
  if TYPE_CHECKING:
51
- from outerproduct_http_types import TaskConfig, TrainerRunRequest
51
+ from outerproduct_http_types import Task, TrainerRunRequest
52
52
 
53
53
  from outerproduct.dataset import Dataset
54
54
  from outerproduct.job import TrainerJob
@@ -105,7 +105,7 @@ class Trainer:
105
105
  self,
106
106
  dataset: Dataset,
107
107
  *,
108
- task: TaskConfig | None,
108
+ task: Task | None,
109
109
  model_types: list[str] | None,
110
110
  metric: Metric | list[Metric] | None,
111
111
  teacher: Model | Predictor | None,
@@ -121,7 +121,7 @@ class Trainer:
121
121
  cls,
122
122
  dataset: Dataset,
123
123
  *,
124
- task: TaskConfig | None = None,
124
+ task: Task | None = None,
125
125
  model_types: list[str] | None = None,
126
126
  metric: Metric | list[Metric] | None = None,
127
127
  teacher: Model | Predictor | None = None,
@@ -132,13 +132,14 @@ class Trainer:
132
132
  ----------
133
133
  dataset : :class:`~outerproduct.dataset.Dataset`
134
134
  Training data.
135
- task : :class:`~outerproduct_http_types.TaskConfig`, optional
135
+ task : :class:`~outerproduct_http_types.Task`, optional
136
136
  The learning task and its target columns — one of
137
- ``RegressionConfig``, ``BinclassConfig``, ``MulticlassConfig``,
138
- ``ForecastingConfig``, or ``SequenceConfig`` (each carries
139
- ``label_column``; the time-series variants also carry
140
- ``id_column`` / ``timestamp_column``). Required unless a
141
- ``teacher`` is given (the teacher then supplies the target).
137
+ ``Regression``, ``Binclass``, ``Multiclass``, ``Forecasting``,
138
+ ``SequenceRegression``, ``SequenceBinclass``, or
139
+ ``SequenceMulticlass`` (each carries ``label_column``; the
140
+ time-series / sequence variants also carry ``id_column`` /
141
+ ``timestamp_column``). Required unless a ``teacher`` is given
142
+ (the teacher then supplies the target).
142
143
  model_types : list of str, optional
143
144
  Candidate model-family identifiers (e.g. ``["tabm",
144
145
  "xgboost"]``). Resolved server-side. Defaults to a curated
@@ -221,7 +222,7 @@ class Trainer:
221
222
 
222
223
  with _client() as client:
223
224
  resp = client.trainer_api.run(request)
224
- self._dataset._stamp_id(resp.dataset_id)
225
+ self._dataset._set_id(resp.dataset_id)
225
226
 
226
227
  label_column = self._task.label_column if self._task is not None else None
227
228
 
@@ -1,16 +0,0 @@
1
- __pycache__/
2
- *.egg-info/
3
- .venv/
4
- .vscode/
5
- .pytest_cache/
6
- .ruff_cache/
7
- .claude/
8
-
9
- .DS_Store
10
- .agents/
11
- skills-lock.json
12
- dist/
13
- site/
14
-
15
- # Solve OUT-294
16
- uv.lock
File without changes
File without changes