Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## [1.7.1] - 2026-10-05

### Changed

- Raise `SeclaiError` from every version-gated list method when a 200 response is not a list: an error-shaped object, text, `null` or an empty body. Most of these methods returned such a body unchanged, and `list_evaluation_criteria()` and `list_run_evaluation_results()` returned `[]` for an empty one
- Read `{"data": null}` as an empty list in every version-gated list method: a list method returns `[]` and a dict method holds `[]` under its documented key. Most of them returned `{"data": None}` unchanged
- Return `{key: [...]}`, under the method's documented key, from a dict-returning version-gated list method that is answered with a bare array. It returned the array, except from `list_evaluation_criteria_page()` and `list_run_evaluation_results_page()`, which already wrapped it
- Change when `paginate()` stops. It now also stops after a page that reports `pagination.has_next` as false or that reaches the `total` the body reports, so a walk whose last page is full can make one request fewer, and after a page holding more than `limit` items unless the body says more exist. A page identical to the one before it is not yielded: the walk raises `SeclaiError` if that page reports more items, and ends if it reports no paging information. Two consecutive pages that are legitimately identical are treated the same way
- Raise `ValueError` from `paginate()` when `limit` is not a positive integer, before any request

### Fixed

- Return the list from `get_agent_callers()`, `list_inbound_email_rejections()`, `list_solution_conversations()`, `list_governance_ai_conversations()`, `list_models()`, `list_memory_bank_templates()` and `get_agents_using_memory_bank()` when `api_version` is `2026-07-27` or later. They returned the `{data, pagination}` object, four of them from a method annotated `list`
- Keep the items under the documented key when `api_version` is `2026-07-27` or later, in `list_knowledge_bases()`, `list_memory_banks()`, `list_agent_email_optouts()`, `list_blocked_email_senders()`, `set_auto_block_mode()`, `list_organization_alert_preferences()`, `list_email_domains()`, `list_alert_configs()`, `list_model_alerts()`, `list_experiments()`, `get_generation_tiers()`, `list_embedding_models()` and `list_reranker_models()`. The items were only under `data`, so `result["knowledge_bases"]` raised `KeyError`. `data` and `pagination` are still present
- Fill the flat `total`, `page` and `limit` a method documents from `pagination` when `api_version` is `2026-07-27` or later. They were absent from the four evaluation listings, the knowledge-base and memory-bank listings and every listing with a `total`
- Declare `attrs` as a runtime dependency. The generated client imports it, so `import seclai` failed with `ModuleNotFoundError` unless another installed package happened to provide `attrs`
- End `paginate()` on an endpoint that ignores `page` and `limit`. `client.paginate("GET", "/alerts/configs", items_key="configs")` never ended on the default API version once an account had 50 alert configs
- Raise `SeclaiError` from `paginate()` when an endpoint that pages by `offset` is walked with the default `param_style="page"` on the default API version. Every request returned the first page, so the walk never ended
- Send one `authorization` and one `x-account-id` on the first typed-method call, such as `list_sources()` or `run_agent()`, when the client uses a bearer-token provider or an SSO profile and `default_headers` spells either header in another case. Both values were sent on that call; later calls sent only the resolved credential, which is now the one sent every time
- Apply the unknown-version guard to a `Seclai-Version` in the default headers of a supplied `http_client`, at construction and on each request. This is a new rejection: a value this release was not built against was sent unchecked and now raises `SeclaiConfigurationError`, as the same value in `default_headers` does. `allow_unknown_api_version=True` permits any value
- Correct the documentation of `list_alert_configs()`: on the default API version it ignores `page` and `limit` and returns every configuration. The README said it paged

## [1.7.0] - 2026-10-05

_Documentation-only release: the `content_version_ids` guidance of `list_source_contents()` now says to keep a request to about 100 ids, since they travel in the query string._

## [1.6.0] - 2026-10-04

### Changed
Expand Down Expand Up @@ -183,6 +209,8 @@ _Stable release. Packaging, CI, and documentation deployment only; no API change

_Initial release._

[1.7.1]: https://github.com/seclai/seclai-python/releases/tag/1.7.1
[1.7.0]: https://github.com/seclai/seclai-python/releases/tag/1.7.0
[1.6.0]: https://github.com/seclai/seclai-python/releases/tag/1.6.0
[1.5.0]: https://github.com/seclai/seclai-python/releases/tag/1.5.0
[1.4.0]: https://github.com/seclai/seclai-python/releases/tag/1.4.0
Expand Down
89 changes: 48 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,45 +174,57 @@ reshape responses, and this client would decode them incorrectly rather than
reject them. Upgrade the package to adopt a new version, or pass
`allow_unknown_api_version=True` if you have to move first and accept that risk.

The guard covers the header however it is supplied — `api_version`,
`default_headers`, or a per-request `headers` argument — and nothing else. An
account pinned server-side can still be
The guard covers the header however it reaches the wire: `api_version`,
`default_headers`, a per-request `headers` argument, or the default headers of
an `http_client` you supply, which are checked at construction and again on
each request, exactly as a value in `default_headers` is. It covers nothing
else. The typed methods that go through the generated client — `run_agent()`,
`list_agent_runs()`, `get_agent_run()`, `delete_agent_run()`, `list_sources()`,
`get_content_detail()`, `delete_content()`, `list_content_embeddings()`,
`upload_file_to_source()` and `upload_file_to_content()` — do not use a supplied
`http_client` at all, so nothing it carries reaches them. An account pinned
server-side can still be
newer than this release — `get_api_version()` reports the `effective_version` the
request resolved to, and comparing it against `LATEST_API_VERSION` is how you
detect the gap.

**What `2026-07-27` changes.** Undeclared query parameters become a 422 instead
of being ignored, and list endpoints move to the canonical
`{"data": [...], "pagination": {...}}` envelope. The affected methods read both
shapes, so they keep working either way — but the metadata moves:
of being ignored, and every list endpoint that answered with a bare array or
under a per-resource key moves to the canonical
`{"data": [...], "pagination": {...}}` envelope. The methods for those endpoints
return what they document on either shape, so code written against the default
still reads the result after you opt in:

| Method | Before | From 2026-07-27 | Legacy paging |
| --- | --- | --- | --- |
| `list_evaluation_criteria_page()` | bare list | `data` + `pagination` | none — returns everything |
| `list_run_evaluation_results_page()` | bare list | `data` + `pagination` | none — returns everything |
| `list_alert_configs()` | `configs` + `total` | `data` + `pagination` | `page` / `limit` |
| `list_model_alerts()` | `alerts` + `total` | `data` + `pagination` | `page` (sent as `offset`) / `limit` |
| `list_experiments()` | `experiments` + `total` | `data` + `pagination` | `limit` / `offset` |
| `get_generation_tiers()` | `tiers` | `data` + `pagination` | none |

`unwrap_items()` reads either shape, so a call site does not have to branch on
the version:

```python
from seclai import unwrap_items

items = unwrap_items(client.list_alert_configs(), "configs")
items = unwrap_items(client.list_model_alerts(), "alerts")
```

Prefer `pagination` over the flat keys. The legacy keys will be deprecated and
then removed once the canonical envelope is the default.

The two evaluation endpoints are **unpaginated** on the legacy shape — they
ignore `page`/`limit` and return everything — so a paginate-until-empty loop over
them only terminates once you have opted in. `get_generation_tiers()` takes no
paging arguments at all and always returns the full set. The remaining three
paginate on either shape.
| Declared return | Methods | From 2026-07-27 |
| --- | --- | --- |
| A list | `list_evaluation_criteria()`, `list_run_evaluation_results()`, `get_agent_callers()`, `list_inbound_email_rejections()`, `list_governance_ai_conversations()`, `list_solution_conversations()`, `list_models()`, `list_memory_bank_templates()`, `get_agents_using_memory_bank()`, `list_cloud_drive_providers()`, `list_cloud_drives()`, `get_agents_using_cloud_drive()`, `list_cloud_drive_rejections()` | Unchanged |
| `data`, from a bare array by default | `list_evaluation_criteria_page()`, `list_run_evaluation_results_page()` | `data`, plus `pagination` |
| `data` with flat `total`/`page`/`limit` | `list_evaluation_results()`, `list_agent_evaluation_results()`, `list_evaluation_runs()`, `list_compatible_runs()` | Unchanged, plus `pagination` |
| A per-resource key | `list_agent_email_optouts()` and `list_blocked_email_senders()` / `set_auto_block_mode()` (`items`), `list_alert_configs()` (`configs`), `list_organization_alert_preferences()` (`preferences`), `list_email_domains()` (`domains`), `list_knowledge_bases()` (`knowledge_bases`), `list_memory_banks()` (`memory_banks`), `list_model_alerts()` (`alerts`), `list_experiments()` (`experiments`), `get_generation_tiers()` (`tiers`), `list_embedding_models()` and `list_reranker_models()` (`models`) | The same key, plus `data` and `pagination` |

Where a method documents flat `total`, `page` or `limit`, the client fills them
from `pagination` after you opt in. Fields that sit beside a list, such as
`auto_block_mode`, the embedding defaults or the email-domain plan capabilities,
are present on both shapes. `pagination` is present only once you opt in, so
read it with `.get("pagination")`.

A 200 response that is not a list at all — an error-shaped object, text, or an
empty body — raises `SeclaiError` from every one of these methods.
`unwrap_items()` still reads either shape of any of these results.

Opting in also turns paging on for endpoints that returned everything by
default, so the same call can return fewer rows:

- `list_evaluation_criteria()` and `list_run_evaluation_results()` return every
item by default and ignore `page`/`limit`. After you opt in they return one
page: 50 items unless you pass `limit`, since this client sends `limit=50`.
The list carries no sign of that; use `list_evaluation_criteria_page()` or
`list_run_evaluation_results_page()` to see `pagination`.
- `list_alert_configs()` ignores `page` and `limit` by default and returns every
config; after you opt in it returns one page of 50.
- `set_auto_block_mode()` returns the first 50 blocked senders on either shape.
Its `total` is the account's full count by default, and the number of rows it
returned after you opt in.

**Later versions.** Each is cumulative, and none changes a response shape this
client decodes:
Expand All @@ -225,11 +237,6 @@ client decodes:
| `2026-09-30` | A run's and a step's `output`, and a step's `input`, are the text rather than a JSON manifest; files are in `attachments` on every version |
| `2026-10-03` | A new LLM step written without `attachments` takes its parent's files, and a new retrieval step's matched media are its files |

The cloud-drive listings and `list_embedding_models()` / `list_reranker_models()`
follow the `2026-07-27` envelope rule as well. The cloud-drive methods return
the items on either shape; read the two model listings with
`unwrap_items(result, "models")`.

## Resources

### Identity
Expand Down Expand Up @@ -702,8 +709,8 @@ print(removed.get("cleanup_note")) # set when the domain was Seclai-managed
tiers = client.get_generation_tiers()

# Embedding and reranker models, with their pricing
embedders = unwrap_items(client.list_embedding_models(), "models")
rerankers = unwrap_items(client.list_reranker_models(), "models")
embedders = client.list_embedding_models()["models"]
rerankers = client.list_reranker_models()["models"]

alerts = client.list_model_alerts()
client.mark_model_alert_read("alert_id")
Expand Down Expand Up @@ -760,7 +767,7 @@ client.submit_ai_feedback({"rating": 5, "comment": "Helpful!"})

### Pagination

All list methods accept `page` and `limit` parameters. For auto-pagination across all pages, use the `paginate` helper:
List methods take the paging arguments their endpoint declares: most take `page` and `limit`, some take `limit` and `offset`, some take `limit` alone, and listings that are always returned whole take none. Each method's signature says which. For auto-pagination across all pages, use the `paginate` helper. It stops after a page that is short or empty, and when the response says there is no next page or its `total` has been reached. A page longer than `limit` also ends it, unless the response says more exist. A page identical to the one before it is not yielded: `paginate` raises `SeclaiError` if that page reports more items — usually the endpoint pages by `offset`, so pass `param_style="offset"` — and otherwise stops:

```python
# Sync — yields items one by one (generator)
Expand Down
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ httpx = "^0.28.1"
# `dateutil.parser.isoparse` for datetime fields, so this must be a runtime
# dependency (types-python-dateutil below only covers typing stubs).
python-dateutil = "^2.8.1"
# Required at runtime by the generated client and models, which are attrs
# classes. The floor is the one openapi-python-client declares; attrs is
# calendar-versioned, so a caret would cap it at the 22.x releases.
attrs = ">=22.2.0"

[tool.poetry.group.dev.dependencies]
pytest = ">=8,<9"
Expand Down
Loading
Loading