frontmost 0.2.1__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,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frontmost AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.5
2
+ Name: frontmost
3
+ Version: 0.2.1
4
+ Summary: See when ChatGPT and other AI apps recommend your business, from your own code and agents.
5
+ Project-URL: Homepage, https://frontmost.ai
6
+ Project-URL: Documentation, https://github.com/frontmost-ai/sdks/tree/main/python#readme
7
+ Project-URL: Repository, https://github.com/frontmost-ai/sdks
8
+ Project-URL: Issues, https://github.com/frontmost-ai/sdks/issues
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: aeo,ai search,ai visibility,chatgpt,frontmost,geo,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: httpx<1,>=0.23
20
+ Requires-Dist: typing-extensions>=4.7
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Frontmost AI for Python
24
+
25
+ Find out when ChatGPT and other AI apps recommend your business, and who they recommend instead. This package gives your code and agents typed access to the [Frontmost AI API](https://app.frontmost.ai/api/v1/openapi.json).
26
+
27
+ ```bash
28
+ pip install https://github.com/frontmost-ai/sdks/releases/download/v0.2.0/frontmost-0.2.0-py3-none-any.whl
29
+ ```
30
+
31
+ Version 0.2.0 installs from [its GitHub release](https://github.com/frontmost-ai/sdks/releases/tag/v0.2.0). The packages go on npm, PyPI, and crates.io next.
32
+
33
+ It runs on Python 3.9 or later. It has a sync client and an async client, and full type hints.
34
+
35
+ ## Quickstart
36
+
37
+ Create a key on [the Team page](https://app.frontmost.ai/team), then put it in `FRONTMOST_API_KEY`.
38
+
39
+ ```python
40
+ from frontmost import Frontmost, format_rate
41
+
42
+ client = Frontmost() # reads FRONTMOST_API_KEY
43
+
44
+ project = client.projects.list().to_list()[0]
45
+ metrics = client.metrics.get(project["id"], period="week", compare_to="previous")
46
+
47
+ totals = metrics["totals"]
48
+ print(format_rate(totals["visibility"] if totals else None)) # 22% · 8 of 36
49
+ ```
50
+
51
+ `format_rate` prints a rate with its counts. A rate never travels without them: `visibility` is `{"value": 0.22, "numerator": 8, "denominator": 36, "previous": 0.17}`.
52
+
53
+ Replies are typed dicts, so your editor knows every key. A full program is in [examples/weekly_report.py](examples/weekly_report.py).
54
+
55
+ ## Read your numbers
56
+
57
+ ```python
58
+ metrics = client.metrics.get(
59
+ "90",
60
+ period="month", # "scan", "week", or "month"
61
+ app="chatgpt", # one AI app, or leave it out for every app
62
+ questions="unbranded", # "all", "branded", or "unbranded"
63
+ group_by="app", # split the numbers by AI app
64
+ )
65
+
66
+ totals = metrics["totals"]
67
+ if totals:
68
+ print(format_rate(totals["win_rate"]))
69
+ for group in metrics.get("groups", []):
70
+ print(group.get("app"), format_rate(group["visibility"]))
71
+ else:
72
+ print(metrics.get("message")) # why there is nothing to count yet, such as no delivered scan
73
+ ```
74
+
75
+ `metrics["scope"]` says exactly what the numbers count: the scan, the dates, the apps, and the question set.
76
+
77
+ ## Who AI names instead of you
78
+
79
+ ```python
80
+ rivals = client.rivals.list("90", period="week").to_list()
81
+
82
+ for rival in sorted(rivals, key=lambda r: r["named_instead_of_you"], reverse=True):
83
+ print(f"{rival['name']}: named instead of you in {rival['named_instead_of_you']} answers")
84
+ ```
85
+
86
+ ## Read the answers
87
+
88
+ Filter the answers, then open one to read its text and citations.
89
+
90
+ ```python
91
+ # Answers that cite reddit.com and do not name you.
92
+ for answer in client.answers.list("90", cited_domain="reddit.com", named=False):
93
+ full = client.answers.get(answer["id"])
94
+ print(full["question"]["text"])
95
+ print(full["text"])
96
+ print([citation["url"] for citation in full["citations"]])
97
+ ```
98
+
99
+ Other filters: `question_id`, `rival_id`, `scan_id`, `period`, `app`, and `questions`.
100
+
101
+ ## Lists and pages
102
+
103
+ A list method returns a page object. You never handle a cursor unless you want to.
104
+
105
+ ```python
106
+ # Every row, one page at a time.
107
+ for question in client.questions.list("90"):
108
+ print(question["text"])
109
+
110
+ # Every row in one list.
111
+ sources = client.sources.list("90", channel="reddit").to_list()
112
+
113
+ # One page, with the list's own fields.
114
+ page = client.rivals.list("90", limit=10).page()
115
+ print(page["scope"], page["has_more"], page["next_cursor"])
116
+ ```
117
+
118
+ ## Change what Frontmost AI tracks
119
+
120
+ ```python
121
+ # Add buyer questions.
122
+ added = client.questions.create(
123
+ "90",
124
+ {"questions": [{"text": "Which AI visibility tool works without a dashboard?", "topic": "Tools"}]},
125
+ )
126
+
127
+ # Pause a question.
128
+ client.questions.update("90", added["data"][0]["id"], {"active": False})
129
+
130
+ # Add a buyer profile.
131
+ client.profiles.create("90", {"name": "Agency owner", "who": "Runs a five-person SEO agency"})
132
+
133
+ # Mark a recommendation as done.
134
+ open_items = client.recommendations.list("90", status="open").to_list()
135
+ client.recommendations.update(open_items[0]["id"], {"status": "done"})
136
+ ```
137
+
138
+ ## Add a project
139
+
140
+ Frontmost AI reads the website before it writes the first questions. `create_and_wait` waits for that and returns the project.
141
+
142
+ ```python
143
+ project = client.projects.create_and_wait({"domain": "example.com", "country": "US"})
144
+ print(project["url"])
145
+ ```
146
+
147
+ `create` returns at once. Its reply is a project, or a setup that tells you when to call again:
148
+
149
+ ```python
150
+ reply = client.projects.create({"domain": "example.com"})
151
+ if reply["object"] == "project_setup":
152
+ ... # Wait reply["retry_after"] seconds, then send reply["next"]["body"] to create again.
153
+ ```
154
+
155
+ `create_and_wait` stops with an error after 10 minutes. The error message names the call that continues the setup.
156
+
157
+ ## Async
158
+
159
+ `AsyncFrontmost` has the same methods. Use `await` for a call and `async for` for a list.
160
+
161
+ ```python
162
+ import asyncio
163
+
164
+ from frontmost import AsyncFrontmost
165
+
166
+
167
+ async def main() -> None:
168
+ async with AsyncFrontmost() as client:
169
+ metrics, rivals = await asyncio.gather(
170
+ client.metrics.get("90", period="week"),
171
+ client.rivals.list("90", period="week").to_list(),
172
+ )
173
+ async for answer in client.answers.list("90", named=False, limit=5):
174
+ print(answer["question"]["text"])
175
+
176
+
177
+ asyncio.run(main())
178
+ ```
179
+
180
+ ## Errors
181
+
182
+ Each API error is an `APIError` subclass with the status, the API's `code`, its message, and the `request_id`.
183
+
184
+ ```python
185
+ from frontmost import APIError, NotFoundError
186
+
187
+ try:
188
+ client.projects.get("12345")
189
+ except NotFoundError as err:
190
+ print(err.message) # the API says what to do next
191
+ except APIError as err:
192
+ print(err.status, err.code, err.request_id)
193
+ ```
194
+
195
+ | Status | Class |
196
+ | --- | --- |
197
+ | 400 | `BadRequestError` |
198
+ | 401 | `AuthenticationError` |
199
+ | 403 | `PermissionDeniedError` |
200
+ | 404 | `NotFoundError` |
201
+ | 409 | `ConflictError` |
202
+ | 422 | `UnprocessableEntityError` |
203
+ | 429 | `RateLimitError` |
204
+ | 5xx | `InternalServerError` |
205
+ | No reply | `APIConnectionError` |
206
+ | No reply in time | `APIConnectionTimeoutError` |
207
+
208
+ All of them extend `FrontmostError`.
209
+
210
+ ## Retries and timeouts
211
+
212
+ The client sends a failed call again up to 2 times: on a timeout, a lost connection, 408, 429, a 5xx, and a 409 that says the same request is still running. It waits as long as `Retry-After` says, up to 60 seconds. With no `Retry-After`, it waits 0.5 s and doubles each time, up to 8 s.
213
+
214
+ Each attempt has 60 seconds. Change either value for the client or for one call:
215
+
216
+ ```python
217
+ client = Frontmost(max_retries=4, timeout=20.0)
218
+
219
+ client.metrics.get("90", max_retries=0, timeout=5.0)
220
+ ```
221
+
222
+ ## Safe writes
223
+
224
+ Each POST carries an `Idempotency-Key`. A retry sends the same key, so the API does the work once. To make your own retries safe too, pass your own key:
225
+
226
+ ```python
227
+ client.questions.create("90", body, idempotency_key="questions-2026-11-16")
228
+ ```
229
+
230
+ ## Configuration
231
+
232
+ | Argument | Default |
233
+ | --- | --- |
234
+ | `api_key` | `FRONTMOST_API_KEY` |
235
+ | `base_url` | `FRONTMOST_BASE_URL`, then `https://app.frontmost.ai/api/v1` |
236
+ | `max_retries` | `2` |
237
+ | `timeout` | `60.0` seconds per attempt |
238
+ | `default_headers` | None |
239
+ | `http_client` | A new `httpx.Client` (`httpx.AsyncClient` for `AsyncFrontmost`) |
240
+
241
+ Every method also takes `max_retries`, `timeout`, `idempotency_key`, and `extra_headers` as keyword arguments. Close the client with `client.close()`, or use it in a `with` block.
242
+
243
+ ## Types
244
+
245
+ Every request and reply has a `TypedDict`, exported by name: `Metrics`, `Rival`, `AnswerSummary`, `Answer`, `Rate`, `Scope`, and the rest. A list of values, such as `Channel`, is a `Literal`. The API can add a value to a list, so handle the values you do not know.
246
+
247
+ ## Versions
248
+
249
+ This package follows [semver](https://semver.org). A new field never breaks your code. A removed or changed field comes only in a new major version, or a new minor version before 1.0. The code is generated from the API's OpenAPI file in [frontmost-ai/sdks](https://github.com/frontmost-ai/sdks).
250
+
251
+ ## License
252
+
253
+ MIT
@@ -0,0 +1,231 @@
1
+ # Frontmost AI for Python
2
+
3
+ Find out when ChatGPT and other AI apps recommend your business, and who they recommend instead. This package gives your code and agents typed access to the [Frontmost AI API](https://app.frontmost.ai/api/v1/openapi.json).
4
+
5
+ ```bash
6
+ pip install https://github.com/frontmost-ai/sdks/releases/download/v0.2.0/frontmost-0.2.0-py3-none-any.whl
7
+ ```
8
+
9
+ Version 0.2.0 installs from [its GitHub release](https://github.com/frontmost-ai/sdks/releases/tag/v0.2.0). The packages go on npm, PyPI, and crates.io next.
10
+
11
+ It runs on Python 3.9 or later. It has a sync client and an async client, and full type hints.
12
+
13
+ ## Quickstart
14
+
15
+ Create a key on [the Team page](https://app.frontmost.ai/team), then put it in `FRONTMOST_API_KEY`.
16
+
17
+ ```python
18
+ from frontmost import Frontmost, format_rate
19
+
20
+ client = Frontmost() # reads FRONTMOST_API_KEY
21
+
22
+ project = client.projects.list().to_list()[0]
23
+ metrics = client.metrics.get(project["id"], period="week", compare_to="previous")
24
+
25
+ totals = metrics["totals"]
26
+ print(format_rate(totals["visibility"] if totals else None)) # 22% · 8 of 36
27
+ ```
28
+
29
+ `format_rate` prints a rate with its counts. A rate never travels without them: `visibility` is `{"value": 0.22, "numerator": 8, "denominator": 36, "previous": 0.17}`.
30
+
31
+ Replies are typed dicts, so your editor knows every key. A full program is in [examples/weekly_report.py](examples/weekly_report.py).
32
+
33
+ ## Read your numbers
34
+
35
+ ```python
36
+ metrics = client.metrics.get(
37
+ "90",
38
+ period="month", # "scan", "week", or "month"
39
+ app="chatgpt", # one AI app, or leave it out for every app
40
+ questions="unbranded", # "all", "branded", or "unbranded"
41
+ group_by="app", # split the numbers by AI app
42
+ )
43
+
44
+ totals = metrics["totals"]
45
+ if totals:
46
+ print(format_rate(totals["win_rate"]))
47
+ for group in metrics.get("groups", []):
48
+ print(group.get("app"), format_rate(group["visibility"]))
49
+ else:
50
+ print(metrics.get("message")) # why there is nothing to count yet, such as no delivered scan
51
+ ```
52
+
53
+ `metrics["scope"]` says exactly what the numbers count: the scan, the dates, the apps, and the question set.
54
+
55
+ ## Who AI names instead of you
56
+
57
+ ```python
58
+ rivals = client.rivals.list("90", period="week").to_list()
59
+
60
+ for rival in sorted(rivals, key=lambda r: r["named_instead_of_you"], reverse=True):
61
+ print(f"{rival['name']}: named instead of you in {rival['named_instead_of_you']} answers")
62
+ ```
63
+
64
+ ## Read the answers
65
+
66
+ Filter the answers, then open one to read its text and citations.
67
+
68
+ ```python
69
+ # Answers that cite reddit.com and do not name you.
70
+ for answer in client.answers.list("90", cited_domain="reddit.com", named=False):
71
+ full = client.answers.get(answer["id"])
72
+ print(full["question"]["text"])
73
+ print(full["text"])
74
+ print([citation["url"] for citation in full["citations"]])
75
+ ```
76
+
77
+ Other filters: `question_id`, `rival_id`, `scan_id`, `period`, `app`, and `questions`.
78
+
79
+ ## Lists and pages
80
+
81
+ A list method returns a page object. You never handle a cursor unless you want to.
82
+
83
+ ```python
84
+ # Every row, one page at a time.
85
+ for question in client.questions.list("90"):
86
+ print(question["text"])
87
+
88
+ # Every row in one list.
89
+ sources = client.sources.list("90", channel="reddit").to_list()
90
+
91
+ # One page, with the list's own fields.
92
+ page = client.rivals.list("90", limit=10).page()
93
+ print(page["scope"], page["has_more"], page["next_cursor"])
94
+ ```
95
+
96
+ ## Change what Frontmost AI tracks
97
+
98
+ ```python
99
+ # Add buyer questions.
100
+ added = client.questions.create(
101
+ "90",
102
+ {"questions": [{"text": "Which AI visibility tool works without a dashboard?", "topic": "Tools"}]},
103
+ )
104
+
105
+ # Pause a question.
106
+ client.questions.update("90", added["data"][0]["id"], {"active": False})
107
+
108
+ # Add a buyer profile.
109
+ client.profiles.create("90", {"name": "Agency owner", "who": "Runs a five-person SEO agency"})
110
+
111
+ # Mark a recommendation as done.
112
+ open_items = client.recommendations.list("90", status="open").to_list()
113
+ client.recommendations.update(open_items[0]["id"], {"status": "done"})
114
+ ```
115
+
116
+ ## Add a project
117
+
118
+ Frontmost AI reads the website before it writes the first questions. `create_and_wait` waits for that and returns the project.
119
+
120
+ ```python
121
+ project = client.projects.create_and_wait({"domain": "example.com", "country": "US"})
122
+ print(project["url"])
123
+ ```
124
+
125
+ `create` returns at once. Its reply is a project, or a setup that tells you when to call again:
126
+
127
+ ```python
128
+ reply = client.projects.create({"domain": "example.com"})
129
+ if reply["object"] == "project_setup":
130
+ ... # Wait reply["retry_after"] seconds, then send reply["next"]["body"] to create again.
131
+ ```
132
+
133
+ `create_and_wait` stops with an error after 10 minutes. The error message names the call that continues the setup.
134
+
135
+ ## Async
136
+
137
+ `AsyncFrontmost` has the same methods. Use `await` for a call and `async for` for a list.
138
+
139
+ ```python
140
+ import asyncio
141
+
142
+ from frontmost import AsyncFrontmost
143
+
144
+
145
+ async def main() -> None:
146
+ async with AsyncFrontmost() as client:
147
+ metrics, rivals = await asyncio.gather(
148
+ client.metrics.get("90", period="week"),
149
+ client.rivals.list("90", period="week").to_list(),
150
+ )
151
+ async for answer in client.answers.list("90", named=False, limit=5):
152
+ print(answer["question"]["text"])
153
+
154
+
155
+ asyncio.run(main())
156
+ ```
157
+
158
+ ## Errors
159
+
160
+ Each API error is an `APIError` subclass with the status, the API's `code`, its message, and the `request_id`.
161
+
162
+ ```python
163
+ from frontmost import APIError, NotFoundError
164
+
165
+ try:
166
+ client.projects.get("12345")
167
+ except NotFoundError as err:
168
+ print(err.message) # the API says what to do next
169
+ except APIError as err:
170
+ print(err.status, err.code, err.request_id)
171
+ ```
172
+
173
+ | Status | Class |
174
+ | --- | --- |
175
+ | 400 | `BadRequestError` |
176
+ | 401 | `AuthenticationError` |
177
+ | 403 | `PermissionDeniedError` |
178
+ | 404 | `NotFoundError` |
179
+ | 409 | `ConflictError` |
180
+ | 422 | `UnprocessableEntityError` |
181
+ | 429 | `RateLimitError` |
182
+ | 5xx | `InternalServerError` |
183
+ | No reply | `APIConnectionError` |
184
+ | No reply in time | `APIConnectionTimeoutError` |
185
+
186
+ All of them extend `FrontmostError`.
187
+
188
+ ## Retries and timeouts
189
+
190
+ The client sends a failed call again up to 2 times: on a timeout, a lost connection, 408, 429, a 5xx, and a 409 that says the same request is still running. It waits as long as `Retry-After` says, up to 60 seconds. With no `Retry-After`, it waits 0.5 s and doubles each time, up to 8 s.
191
+
192
+ Each attempt has 60 seconds. Change either value for the client or for one call:
193
+
194
+ ```python
195
+ client = Frontmost(max_retries=4, timeout=20.0)
196
+
197
+ client.metrics.get("90", max_retries=0, timeout=5.0)
198
+ ```
199
+
200
+ ## Safe writes
201
+
202
+ Each POST carries an `Idempotency-Key`. A retry sends the same key, so the API does the work once. To make your own retries safe too, pass your own key:
203
+
204
+ ```python
205
+ client.questions.create("90", body, idempotency_key="questions-2026-11-16")
206
+ ```
207
+
208
+ ## Configuration
209
+
210
+ | Argument | Default |
211
+ | --- | --- |
212
+ | `api_key` | `FRONTMOST_API_KEY` |
213
+ | `base_url` | `FRONTMOST_BASE_URL`, then `https://app.frontmost.ai/api/v1` |
214
+ | `max_retries` | `2` |
215
+ | `timeout` | `60.0` seconds per attempt |
216
+ | `default_headers` | None |
217
+ | `http_client` | A new `httpx.Client` (`httpx.AsyncClient` for `AsyncFrontmost`) |
218
+
219
+ Every method also takes `max_retries`, `timeout`, `idempotency_key`, and `extra_headers` as keyword arguments. Close the client with `client.close()`, or use it in a `with` block.
220
+
221
+ ## Types
222
+
223
+ Every request and reply has a `TypedDict`, exported by name: `Metrics`, `Rival`, `AnswerSummary`, `Answer`, `Rate`, `Scope`, and the rest. A list of values, such as `Channel`, is a `Literal`. The API can add a value to a list, so handle the values you do not know.
224
+
225
+ ## Versions
226
+
227
+ This package follows [semver](https://semver.org). A new field never breaks your code. A removed or changed field comes only in a new major version, or a new minor version before 1.0. The code is generated from the API's OpenAPI file in [frontmost-ai/sdks](https://github.com/frontmost-ai/sdks).
228
+
229
+ ## License
230
+
231
+ MIT
@@ -0,0 +1,28 @@
1
+ """Print this week's numbers for one project, the rivals that AI apps name instead of you,
2
+ and the answers that cite reddit.com but do not name you.
3
+
4
+ FRONTMOST_API_KEY=fmk_... python examples/weekly_report.py 90
5
+ """
6
+
7
+ import sys
8
+
9
+ from frontmost import Frontmost, format_rate
10
+
11
+ project_id = sys.argv[1] if len(sys.argv) > 1 else "90"
12
+ client = Frontmost()
13
+
14
+ metrics = client.metrics.get(project_id, period="week", compare_to="previous")
15
+ totals = metrics["totals"]
16
+ if totals is None:
17
+ print(metrics.get("message"))
18
+ sys.exit(0)
19
+ print(f"Visibility {format_rate(totals['visibility'])}")
20
+ print(f"Win rate {format_rate(totals['win_rate'])}")
21
+
22
+ rivals = client.rivals.list(project_id, period="week").to_list()
23
+ rivals.sort(key=lambda rival: rival["named_instead_of_you"], reverse=True)
24
+ for rival in rivals[:3]:
25
+ print(f"{rival['name']} was named instead of you in {rival['named_instead_of_you']} answers")
26
+
27
+ for answer in client.answers.list(project_id, cited_domain="reddit.com", named=False, limit=5):
28
+ print(f"{answer['app']}: {answer['question']['text']}")
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "frontmost"
3
+ version = "0.2.1"
4
+ description = "See when ChatGPT and other AI apps recommend your business, from your own code and agents."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.9"
9
+ keywords = ["frontmost", "ai visibility", "aeo", "geo", "chatgpt", "ai search", "sdk"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Typing :: Typed",
17
+ ]
18
+ dependencies = ["httpx>=0.23,<1", "typing-extensions>=4.7"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://frontmost.ai"
22
+ Documentation = "https://github.com/frontmost-ai/sdks/tree/main/python#readme"
23
+ Repository = "https://github.com/frontmost-ai/sdks"
24
+ Issues = "https://github.com/frontmost-ai/sdks/issues"
25
+
26
+ [dependency-groups]
27
+ dev = ["pytest>=8", "mypy>=1.13"]
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/frontmost"]
35
+
36
+ [tool.mypy]
37
+ strict = true
38
+
39
+ [tool.ruff]
40
+ target-version = "py39"
41
+ line-length = 120
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "F", "I", "UP", "B"]
45
+ # One-line exception classes read best as one line.
46
+ ignore = ["E701"]
@@ -0,0 +1,21 @@
1
+ """The Frontmost AI API client. `from frontmost import Frontmost` and `Frontmost()` reads FRONTMOST_API_KEY."""
2
+
3
+ from ._core import VERSION as VERSION
4
+ from ._core import APIConnectionError as APIConnectionError
5
+ from ._core import APIConnectionTimeoutError as APIConnectionTimeoutError
6
+ from ._core import APIError as APIError
7
+ from ._core import AsyncPage as AsyncPage
8
+ from ._core import AuthenticationError as AuthenticationError
9
+ from ._core import BadRequestError as BadRequestError
10
+ from ._core import ConflictError as ConflictError
11
+ from ._core import FrontmostError as FrontmostError
12
+ from ._core import InternalServerError as InternalServerError
13
+ from ._core import ListPage as ListPage
14
+ from ._core import NotFoundError as NotFoundError
15
+ from ._core import PermissionDeniedError as PermissionDeniedError
16
+ from ._core import RateLimitError as RateLimitError
17
+ from ._core import RequestOptions as RequestOptions
18
+ from ._core import SyncPage as SyncPage
19
+ from ._core import UnprocessableEntityError as UnprocessableEntityError
20
+ from ._core import format_rate as format_rate
21
+ from ._generated import * # noqa: F403 (the generated module lists its names in __all__)