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
2 changes: 1 addition & 1 deletion .spec-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6e8f07d1484ca4c4b976609f61c1d88b8288b3a3
8c8c6f7f8bdffec317e65c2abf9e2af93692424f
33 changes: 33 additions & 0 deletions guards.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
........................................................................ [ 17%]
..................................................................s..... [ 34%]
........................................................................ [ 51%]
.........F.............................................................. [ 68%]
........................................................................ [ 85%]
............................................................... [100%]
=================================== FAILURES ===================================
__________________ test_no_unexposed_sdk_params[voices.list] ___________________

cmd = Cmd(group='voices', name='list', method='voices.list', help='List available voices.', subgroup=None, args=[], options=...ider', 'gender', 'category'], success_msg=None, destructive=False, redact=False, fallback_methods=(), gate='buildable')

@pytest.mark.parametrize("cmd", TABLE, ids=lambda c: ".".join(c.path))
def test_no_unexposed_sdk_params(cmd: Cmd) -> None:
"""Every param the SDK accepts is reachable from the CLI, or allowlisted with a reason.

This is the drift check the regen PR runs: the generated client gains a param, no
hand-written flag gains with it, and this goes red naming the command and the param
instead of leaving the gap to be found in use months later.
"""
missing = _unexposed_params(cmd) - set(_INTENTIONALLY_UNEXPOSED.get(cmd.path, {}))

> assert not missing, (
f"{'.'.join(cmd.path)}: {cmd.method} accepts {sorted(missing)} but the CLI has no flag "
f"for it. Either add an Opt(...) to the row in _spec.py, or record the decision in "
f"_INTENTIONALLY_UNEXPOSED with the reason."
)
E AssertionError: voices.list: voices.list accepts ['provider_voice_id'] but the CLI has no flag for it. Either add an Opt(...) to the row in _spec.py, or record the decision in _INTENTIONALLY_UNEXPOSED with the reason.
E assert not {'provider_voice_id'}

tests/build/test_sdk_contract.py:233: AssertionError
=========================== short test summary info ============================
FAILED tests/build/test_sdk_contract.py::test_no_unexposed_sdk_params[voices.list] - AssertionError: voices.list: voices.list accepts ['provider_voice_id'] but the CLI has no flag for it. Either add an Opt(...) to the row in _spec.py, or record the decision in _INTENTIONALLY_UNEXPOSED with the reason.
assert not {'provider_voice_id'}
7 changes: 4 additions & 3 deletions src/onepin/.fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
}
]
},
"originGitCommit": "3d712ffd87d0eab81a87aa8c0e603081a11ee5f1",
"originGitCommitIsDirty": true,
"invokedBy": "manual"
"originGitCommit": "45e96724ab4ffc17fdc44d6f21e1f71206c318cc",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
"ciProvider": "github"
}
24 changes: 0 additions & 24 deletions src/onepin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ The Onepin Python library provides convenient access to the Onepin APIs from Pyt
- [Environments](#environments)
- [Async Client](#async-client)
- [Exception Handling](#exception-handling)
- [Pagination](#pagination)
- [Advanced](#advanced)
- [Access Raw Response Data](#access-raw-response-data)
- [Retries](#retries)
Expand Down Expand Up @@ -102,29 +101,6 @@ except ApiError as e:
print(e.body)
```

## Pagination

Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object.

```python
from onepin import OnePinClient

client = OnePinClient(
token="<token>",
)

client.templates.list()
```

```python
# You can also iterate through pages and access the typed response per page
pager = client.templates.list(...)
for page in pager.iter_pages():
print(page.response) # access the typed response for each page
for item in page:
print(item)
```

## Advanced

### Access Raw Response Data
Expand Down
6 changes: 6 additions & 0 deletions src/onepin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ApiListResponseTemplateOut,
ApiListResponseUploadOut,
ApiListResponseUsageActivityOut,
ApiListResponseVoiceRecommendedOut,
ApiListResponseVoiceSimilarOut,
ApiListResponseWorkspaceMemberOut,
ApiListResponseWorkspaceOut,
Expand Down Expand Up @@ -169,6 +170,7 @@
VoiceModelCapabilityOut,
VoiceOut,
VoicePreviewOut,
VoiceRecommendedOut,
VoiceSimilarOut,
VoiceSource,
WorkflowDefinitionInput,
Expand Down Expand Up @@ -316,6 +318,7 @@
"ApiListResponseTemplateOut": ".types",
"ApiListResponseUploadOut": ".types",
"ApiListResponseUsageActivityOut": ".types",
"ApiListResponseVoiceRecommendedOut": ".types",
"ApiListResponseVoiceSimilarOut": ".types",
"ApiListResponseWorkspaceMemberOut": ".types",
"ApiListResponseWorkspaceOut": ".types",
Expand Down Expand Up @@ -500,6 +503,7 @@
"VoiceModelCapabilityOut": ".types",
"VoiceOut": ".types",
"VoicePreviewOut": ".types",
"VoiceRecommendedOut": ".types",
"VoiceSimilarOut": ".types",
"VoiceSource": ".types",
"WorkflowDefinitionInput": ".types",
Expand Down Expand Up @@ -636,6 +640,7 @@ def __dir__():
"ApiListResponseTemplateOut",
"ApiListResponseUploadOut",
"ApiListResponseUsageActivityOut",
"ApiListResponseVoiceRecommendedOut",
"ApiListResponseVoiceSimilarOut",
"ApiListResponseWorkspaceMemberOut",
"ApiListResponseWorkspaceOut",
Expand Down Expand Up @@ -820,6 +825,7 @@ def __dir__():
"VoiceModelCapabilityOut",
"VoiceOut",
"VoicePreviewOut",
"VoiceRecommendedOut",
"VoiceSimilarOut",
"VoiceSource",
"WorkflowDefinitionInput",
Expand Down
15 changes: 15 additions & 0 deletions src/onepin/_cli/_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,21 @@ def _literals(alias: Any) -> tuple[str, ...]:
'unhurried narrator"), not just a name.'
),
),
Opt(
# Plain string, no `wrap_list`: unlike --provider next to it, the SDK keyword is a
# single `str`, because one wire id is one id. Two rows may still answer to it.
"--provider-voice-id",
"str",
None,
help=(
"Filter to the exact vendor wire id (the value submitted to the provider for "
"synthesis). An exact match, not a substring, and invisible to --search: the "
"two AND rather than one widening the other. The same id can be held by more "
"than one visible row — your own imported voice and the platform catalog row "
"— so this can still return several voices; add --provider to narrow to one "
"vendor. Ranking is skipped, so --sort/--order apply as on a plain browse."
),
),
Opt(
"--sort",
_literals(ListVoicesRequestSortItem),
Expand Down
11 changes: 6 additions & 5 deletions src/onepin/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(
self._max_stream_reconnection_attempts = max_stream_reconnection_attempts
self._logging = logging

def get_headers(self) -> typing.Dict[str, str]:
def get_headers(self, *, include_token: bool = True) -> typing.Dict[str, str]:
import platform

headers: typing.Dict[str, str] = {
Expand All @@ -39,9 +39,10 @@ def get_headers(self) -> typing.Dict[str, str]:
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
**(self.get_custom_headers() or {}),
}
token = self._get_token()
if token is not None:
headers["Authorization"] = f"Bearer {token}"
if include_token:
token = self._get_token()
if token is not None:
headers["Authorization"] = f"Bearer {token}"
return headers

def _get_token(self) -> typing.Optional[str]:
Expand Down Expand Up @@ -140,7 +141,7 @@ def __init__(
)

async def async_get_headers(self) -> typing.Dict[str, str]:
headers = self.get_headers()
headers = self.get_headers(include_token=self._async_token is None)
if self._async_token is not None:
token = await self._async_token()
headers["Authorization"] = f"Bearer {token}"
Expand Down
Loading
Loading