ventimath 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: ventimath
3
+ Version: 0.1.0
4
+ Summary: Small standard-library client for VentiMath answer keys and profile skill tags
5
+ Project-URL: Repository, https://github.com/VentiMath/venti-sdk
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # VentiMath Python SDK
10
+
11
+ Download answer keys, import your problem classifications, and find problems by skill.
12
+ Requires Python 3.10 or newer, with no runtime dependencies.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pip install ventimath
18
+ # Or add it to a uv project:
19
+ uv add ventimath
20
+ ```
21
+
22
+ The default API base URL is
23
+ `https://us-central1-ventimath.cloudfunctions.net`; use `base_url=` for a deployed
24
+ test environment or functions emulator.
25
+
26
+ ## Download answer keys for the existing classifier
27
+
28
+ Answer-key reads are public and always omit authentication, even when the client
29
+ has a token. Download a file; the classifier's existing `--answer-key` reads a
30
+ local JSON path, not a URL.
31
+
32
+ ```python
33
+ from ventimath import VentiMathClient
34
+
35
+ public = VentiMathClient()
36
+ result = public.download_answer_key("answer-key.json", exam="EXAM_ID")
37
+ print("Missing problems:", result["missing"])
38
+ print("No usable key:", result["unavailable"])
39
+
40
+ # Or select up to 100 canonical problem IDs:
41
+ keys = public.answer_keys(ids=["PROBLEM_ID_1", "PROBLEM_ID_2"])
42
+ ```
43
+
44
+ ```sh
45
+ python classify_skills.py PROBLEM_ID --answer-key answer-key.json
46
+ ```
47
+
48
+ The response and saved file have the same shape:
49
+
50
+ ```json
51
+ {
52
+ "problems": {
53
+ "PROBLEM_ID": {"correctChoice": "B", "answerText": "2"}
54
+ },
55
+ "missing": [],
56
+ "unavailable": []
57
+ }
58
+ ```
59
+
60
+ Text-answer records carry `answerText`; accepted alternatives may also appear in
61
+ `allowedAnswers`. Exam requests are bounded to 500 placements. Unknown canonical
62
+ IDs appear in `missing`; proofs, absent keys, and placeholders appear in
63
+ `unavailable`.
64
+
65
+ ## Import classifications on your own profile
66
+
67
+ Create your VentiMath account and mint **your own** personal access token in
68
+ Settings → Developer. Select `tags.read` and `tags.write`. Keep the token in your
69
+ environment rather than in source files. The authenticated account owns the set;
70
+ there is no option to assign your upload to someone else's account.
71
+
72
+ `VentiMathClient(token=...)` uses an explicit token. `VentiMathClient.from_env()`
73
+ reads `VENTI_API_KEY` and optionally `VENTI_API_URL`. Neither stores credentials.
74
+ The ordinary `VentiMathClient()` constructor remains anonymous even if a token
75
+ environment variable exists.
76
+
77
+ Sets are **public by default**. Pass `public=False` at creation to keep a set
78
+ private to your account. Publishing a private set is permanent; public sets cannot
79
+ be made private. This import example creates a private set:
80
+
81
+ ```python
82
+ from pathlib import Path
83
+ from ventimath import (
84
+ VentiMathClient,
85
+ load_classification_files,
86
+ load_vocabulary_csv,
87
+ )
88
+
89
+ client = VentiMathClient.from_env()
90
+ vocabulary = load_vocabulary_csv("skill-lists/skills-merged.csv")
91
+ created = client.create_tag_set(
92
+ "amc-skills",
93
+ "My AMC skill classifications",
94
+ public=False,
95
+ vocabulary=vocabulary,
96
+ )
97
+ set_id = created["set"]["id"]
98
+
99
+ # Use saved per-problem output from a single classifier run folder.
100
+ records = load_classification_files(sorted(Path("output/skills/MODEL_DIRECTORY").glob("*.json")))
101
+
102
+ # Or use the classifier's combined output instead:
103
+ # records = load_classification_files(["combined.json"])
104
+
105
+ preview = client.import_classifications(set_id, records, dry_run=True)
106
+ print(preview)
107
+
108
+ # After inspecting the preview, upload the same records.
109
+ report = client.import_classifications(set_id, records)
110
+ print("Imported:", report["imported"], "unchanged:", report["unchanged"])
111
+ for rejected in report["rejected"]:
112
+ print(rejected)
113
+ ```
114
+
115
+ Individual records use `{problemId, result, model, reasoningEffort}`. Combined
116
+ files use `{problemId, llmResult}` and usually omit model provenance; supply
117
+ `model="MODEL_USED"` and `reasoning_effort="EFFORT_USED"` to `import_classifications`
118
+ when those values are known. Per-record values take precedence. Both shapes pass
119
+ through to the API unchanged. The server validates each classification and
120
+ discards copied prompts, raw replies, statements, and answer fields.
121
+
122
+ Vocabulary CSV headers are `category`, `skill`, and `description`, in any order.
123
+ Quoted commas/newlines, escaped quotes, and a UTF-8 BOM are supported. On import,
124
+ `vocabulary=` merges by skill name; supplied entries update matching descriptions
125
+ and categories. Accepted records add unseen skill names. To explicitly replace
126
+ the vocabulary, use `update_tag_set(set_id, vocabulary=...)`.
127
+
128
+ ## Retrieve problems by skill
129
+
130
+ ```python
131
+ # Your own sets, including private sets; needs tags.read.
132
+ mine = client.list_tag_sets()
133
+
134
+ # Public discovery always omits the token, even on an authenticated client.
135
+ shared = client.list_public_tag_sets(owner="OWNER_NUMERIC_ID", limit=20)
136
+ # Pass shared["nextCursor"] as cursor= to request the next page.
137
+
138
+ # Set listing returns metadata. Fetch a set to get its vocabulary.
139
+ tag_set = client.get_tag_set(set_id)
140
+ print(tag_set["set"]["vocabulary"])
141
+
142
+ # One page, preserving the response envelope and pagination cursor:
143
+ page = client.tagged_problems(
144
+ set_id, skill="Casework", importance="essential", limit=25
145
+ )
146
+
147
+ # Or iterate across every page:
148
+ for problem in client.iter_tagged_problems(set_id, skill="Casework"):
149
+ print(problem["id"], problem["details"]["statement"])
150
+ print(problem["classification"])
151
+ ```
152
+
153
+ Skill names match exactly. Importance is `essential`, `time-saving`, or `helpful`
154
+ and requires a skill filter.
155
+ Use `public=True` on `get_tag_set`, `tagged_problems`, or `iter_tagged_problems`
156
+ to read a public set without sending the client's token. Public problem records
157
+ omit catalog answer fields; their attached classifications remain attributed to
158
+ the set owner.
159
+
160
+ ## Reports, errors, and retries
161
+
162
+ Imports are split into requests of at most 200 records and 2 MiB of UTF-8 JSON.
163
+ All input is checked for JSON encodability and individual request size before
164
+ any request is sent. The aggregate response contains `imported`, `unchanged`,
165
+ `rejected`, `dryRun`, `total`, `processed`, and `remaining`. Rejected entries retain
166
+ the server's error code and problem ID; their zero-based `index` refers to your
167
+ complete input sequence. Row rejections do not raise an exception; inspect them.
168
+
169
+ Dry runs validate each batch against the currently stored set without writing.
170
+ For imports spanning batches, repeated problem IDs or the cumulative vocabulary
171
+ limit can make actual counts differ from the preview. Inspect the final report.
172
+
173
+ ```python
174
+ from ventimath import VentiMathAPIError, VentiMathImportError
175
+
176
+ try:
177
+ report = client.import_classifications(set_id, records)
178
+ except VentiMathImportError as error:
179
+ print("Completed requests:", error.report)
180
+ print("HTTP status, if available:", error.status)
181
+ print("Server payload, if available:", error.payload)
182
+ # A failed response does not prove the last request was uncommitted.
183
+ # Retrying the identical input is safe; unchanged records are not rewritten.
184
+ except VentiMathAPIError as error:
185
+ print(error.status, error.payload)
186
+ ```
187
+
188
+ Other methods raise `VentiMathAPIError` with the original HTTP status and server
189
+ payload for HTTP failures. `VentiMathConnectionError` covers transport failures,
190
+ including interrupted response bodies;
191
+ `VentiMathProtocolError` covers unexpected responses. Invalid local arguments
192
+ raise `ValueError`. Imports wrap transport/protocol/HTTP failures in
193
+ `VentiMathImportError` to preserve the counts from completed requests.
194
+
195
+ These methods call the actual REST routes: `GET /answerKeys`,
196
+ `GET|POST /problemTags/sets`, `GET|PATCH /problemTags/sets/{id}`,
197
+ `POST /problemTags/sets/{id}/import`, and
198
+ `GET /problemTags/sets/{id}/problems`. You can call those endpoints directly from
199
+ your existing Python HTTP code; the SDK is optional.
200
+
201
+ ## Development
202
+
203
+ Install from a source checkout with `python -m pip install .`.
204
+
205
+ From this package directory, without installing or contacting a server:
206
+
207
+ ```sh
208
+ PYTHONPATH=src python -B -m unittest discover -s tests -v
209
+ ```
210
+
211
+ All transport calls in these tests are mocked. They exercise exact URLs and
212
+ headers, anonymous discovery, private ownership request shaping, both classifier
213
+ record formats, CSV parsing, Unicode batch limits, pagination, rejected redirects,
214
+ and partial-failure reports.
@@ -0,0 +1,206 @@
1
+ # VentiMath Python SDK
2
+
3
+ Download answer keys, import your problem classifications, and find problems by skill.
4
+ Requires Python 3.10 or newer, with no runtime dependencies.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ pip install ventimath
10
+ # Or add it to a uv project:
11
+ uv add ventimath
12
+ ```
13
+
14
+ The default API base URL is
15
+ `https://us-central1-ventimath.cloudfunctions.net`; use `base_url=` for a deployed
16
+ test environment or functions emulator.
17
+
18
+ ## Download answer keys for the existing classifier
19
+
20
+ Answer-key reads are public and always omit authentication, even when the client
21
+ has a token. Download a file; the classifier's existing `--answer-key` reads a
22
+ local JSON path, not a URL.
23
+
24
+ ```python
25
+ from ventimath import VentiMathClient
26
+
27
+ public = VentiMathClient()
28
+ result = public.download_answer_key("answer-key.json", exam="EXAM_ID")
29
+ print("Missing problems:", result["missing"])
30
+ print("No usable key:", result["unavailable"])
31
+
32
+ # Or select up to 100 canonical problem IDs:
33
+ keys = public.answer_keys(ids=["PROBLEM_ID_1", "PROBLEM_ID_2"])
34
+ ```
35
+
36
+ ```sh
37
+ python classify_skills.py PROBLEM_ID --answer-key answer-key.json
38
+ ```
39
+
40
+ The response and saved file have the same shape:
41
+
42
+ ```json
43
+ {
44
+ "problems": {
45
+ "PROBLEM_ID": {"correctChoice": "B", "answerText": "2"}
46
+ },
47
+ "missing": [],
48
+ "unavailable": []
49
+ }
50
+ ```
51
+
52
+ Text-answer records carry `answerText`; accepted alternatives may also appear in
53
+ `allowedAnswers`. Exam requests are bounded to 500 placements. Unknown canonical
54
+ IDs appear in `missing`; proofs, absent keys, and placeholders appear in
55
+ `unavailable`.
56
+
57
+ ## Import classifications on your own profile
58
+
59
+ Create your VentiMath account and mint **your own** personal access token in
60
+ Settings → Developer. Select `tags.read` and `tags.write`. Keep the token in your
61
+ environment rather than in source files. The authenticated account owns the set;
62
+ there is no option to assign your upload to someone else's account.
63
+
64
+ `VentiMathClient(token=...)` uses an explicit token. `VentiMathClient.from_env()`
65
+ reads `VENTI_API_KEY` and optionally `VENTI_API_URL`. Neither stores credentials.
66
+ The ordinary `VentiMathClient()` constructor remains anonymous even if a token
67
+ environment variable exists.
68
+
69
+ Sets are **public by default**. Pass `public=False` at creation to keep a set
70
+ private to your account. Publishing a private set is permanent; public sets cannot
71
+ be made private. This import example creates a private set:
72
+
73
+ ```python
74
+ from pathlib import Path
75
+ from ventimath import (
76
+ VentiMathClient,
77
+ load_classification_files,
78
+ load_vocabulary_csv,
79
+ )
80
+
81
+ client = VentiMathClient.from_env()
82
+ vocabulary = load_vocabulary_csv("skill-lists/skills-merged.csv")
83
+ created = client.create_tag_set(
84
+ "amc-skills",
85
+ "My AMC skill classifications",
86
+ public=False,
87
+ vocabulary=vocabulary,
88
+ )
89
+ set_id = created["set"]["id"]
90
+
91
+ # Use saved per-problem output from a single classifier run folder.
92
+ records = load_classification_files(sorted(Path("output/skills/MODEL_DIRECTORY").glob("*.json")))
93
+
94
+ # Or use the classifier's combined output instead:
95
+ # records = load_classification_files(["combined.json"])
96
+
97
+ preview = client.import_classifications(set_id, records, dry_run=True)
98
+ print(preview)
99
+
100
+ # After inspecting the preview, upload the same records.
101
+ report = client.import_classifications(set_id, records)
102
+ print("Imported:", report["imported"], "unchanged:", report["unchanged"])
103
+ for rejected in report["rejected"]:
104
+ print(rejected)
105
+ ```
106
+
107
+ Individual records use `{problemId, result, model, reasoningEffort}`. Combined
108
+ files use `{problemId, llmResult}` and usually omit model provenance; supply
109
+ `model="MODEL_USED"` and `reasoning_effort="EFFORT_USED"` to `import_classifications`
110
+ when those values are known. Per-record values take precedence. Both shapes pass
111
+ through to the API unchanged. The server validates each classification and
112
+ discards copied prompts, raw replies, statements, and answer fields.
113
+
114
+ Vocabulary CSV headers are `category`, `skill`, and `description`, in any order.
115
+ Quoted commas/newlines, escaped quotes, and a UTF-8 BOM are supported. On import,
116
+ `vocabulary=` merges by skill name; supplied entries update matching descriptions
117
+ and categories. Accepted records add unseen skill names. To explicitly replace
118
+ the vocabulary, use `update_tag_set(set_id, vocabulary=...)`.
119
+
120
+ ## Retrieve problems by skill
121
+
122
+ ```python
123
+ # Your own sets, including private sets; needs tags.read.
124
+ mine = client.list_tag_sets()
125
+
126
+ # Public discovery always omits the token, even on an authenticated client.
127
+ shared = client.list_public_tag_sets(owner="OWNER_NUMERIC_ID", limit=20)
128
+ # Pass shared["nextCursor"] as cursor= to request the next page.
129
+
130
+ # Set listing returns metadata. Fetch a set to get its vocabulary.
131
+ tag_set = client.get_tag_set(set_id)
132
+ print(tag_set["set"]["vocabulary"])
133
+
134
+ # One page, preserving the response envelope and pagination cursor:
135
+ page = client.tagged_problems(
136
+ set_id, skill="Casework", importance="essential", limit=25
137
+ )
138
+
139
+ # Or iterate across every page:
140
+ for problem in client.iter_tagged_problems(set_id, skill="Casework"):
141
+ print(problem["id"], problem["details"]["statement"])
142
+ print(problem["classification"])
143
+ ```
144
+
145
+ Skill names match exactly. Importance is `essential`, `time-saving`, or `helpful`
146
+ and requires a skill filter.
147
+ Use `public=True` on `get_tag_set`, `tagged_problems`, or `iter_tagged_problems`
148
+ to read a public set without sending the client's token. Public problem records
149
+ omit catalog answer fields; their attached classifications remain attributed to
150
+ the set owner.
151
+
152
+ ## Reports, errors, and retries
153
+
154
+ Imports are split into requests of at most 200 records and 2 MiB of UTF-8 JSON.
155
+ All input is checked for JSON encodability and individual request size before
156
+ any request is sent. The aggregate response contains `imported`, `unchanged`,
157
+ `rejected`, `dryRun`, `total`, `processed`, and `remaining`. Rejected entries retain
158
+ the server's error code and problem ID; their zero-based `index` refers to your
159
+ complete input sequence. Row rejections do not raise an exception; inspect them.
160
+
161
+ Dry runs validate each batch against the currently stored set without writing.
162
+ For imports spanning batches, repeated problem IDs or the cumulative vocabulary
163
+ limit can make actual counts differ from the preview. Inspect the final report.
164
+
165
+ ```python
166
+ from ventimath import VentiMathAPIError, VentiMathImportError
167
+
168
+ try:
169
+ report = client.import_classifications(set_id, records)
170
+ except VentiMathImportError as error:
171
+ print("Completed requests:", error.report)
172
+ print("HTTP status, if available:", error.status)
173
+ print("Server payload, if available:", error.payload)
174
+ # A failed response does not prove the last request was uncommitted.
175
+ # Retrying the identical input is safe; unchanged records are not rewritten.
176
+ except VentiMathAPIError as error:
177
+ print(error.status, error.payload)
178
+ ```
179
+
180
+ Other methods raise `VentiMathAPIError` with the original HTTP status and server
181
+ payload for HTTP failures. `VentiMathConnectionError` covers transport failures,
182
+ including interrupted response bodies;
183
+ `VentiMathProtocolError` covers unexpected responses. Invalid local arguments
184
+ raise `ValueError`. Imports wrap transport/protocol/HTTP failures in
185
+ `VentiMathImportError` to preserve the counts from completed requests.
186
+
187
+ These methods call the actual REST routes: `GET /answerKeys`,
188
+ `GET|POST /problemTags/sets`, `GET|PATCH /problemTags/sets/{id}`,
189
+ `POST /problemTags/sets/{id}/import`, and
190
+ `GET /problemTags/sets/{id}/problems`. You can call those endpoints directly from
191
+ your existing Python HTTP code; the SDK is optional.
192
+
193
+ ## Development
194
+
195
+ Install from a source checkout with `python -m pip install .`.
196
+
197
+ From this package directory, without installing or contacting a server:
198
+
199
+ ```sh
200
+ PYTHONPATH=src python -B -m unittest discover -s tests -v
201
+ ```
202
+
203
+ All transport calls in these tests are mocked. They exercise exact URLs and
204
+ headers, anonymous discovery, private ownership request shaping, both classifier
205
+ record formats, CSV parsing, Unicode batch limits, pagination, rejected redirects,
206
+ and partial-failure reports.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ventimath"
7
+ version = "0.1.0"
8
+ description = "Small standard-library client for VentiMath answer keys and profile skill tags"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+
13
+ [project.urls]
14
+ Repository = "https://github.com/VentiMath/venti-sdk"
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
18
+
19
+ [tool.ruff]
20
+ line-length = 100
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ """VentiMath REST client. No inference providers or runtime dependencies."""
2
+
3
+ from .client import (
4
+ DEFAULT_BASE_URL,
5
+ VentiMathAPIError,
6
+ VentiMathClient,
7
+ VentiMathConnectionError,
8
+ VentiMathError,
9
+ VentiMathImportError,
10
+ VentiMathProtocolError,
11
+ load_classification_files,
12
+ load_vocabulary_csv,
13
+ )
14
+
15
+ __all__ = [
16
+ "DEFAULT_BASE_URL",
17
+ "VentiMathAPIError",
18
+ "VentiMathClient",
19
+ "VentiMathConnectionError",
20
+ "VentiMathError",
21
+ "VentiMathImportError",
22
+ "VentiMathProtocolError",
23
+ "load_classification_files",
24
+ "load_vocabulary_csv",
25
+ ]