diff --git a/.github/fern/generators.yml b/.github/fern/generators.yml index 2829cb8b7..68d0916a1 100644 --- a/.github/fern/generators.yml +++ b/.github/fern/generators.yml @@ -100,3 +100,30 @@ groups: streamType: web useDefaultRequestParameterValues: true smart-casing: true + # `local-file-system` allows no `github` block, so the workflow commits the result itself. + python-sdk: + generators: + - name: fernapi/fern-python-sdk + version: 5.29.0 + output: + location: local-file-system + path: ../../python/trueforge_sdk + config: + # Import package (underscores). PyPI/poetry name trueforge-sdk is set in + # scripts/generate-sdk.sh after generate (local-file-system has no package-name). + package_name: trueforge_sdk + optional_auth: true + stream_abstraction: true + use_request_defaults: all + inline_path_params: true + pydantic_config: + enum_type: forward_compatible_python_enums + frozen: false + skip_validation: true + client: + class_name: BaseTrueForge + filename: base_client.py + exported_class_name: TrueForge + exported_filename: client.py + pyproject_python_version: '>=3.10' + smart-casing: true diff --git a/.github/workflows/generate-sdk.yaml b/.github/workflows/generate-sdk.yaml index 7efa814e8..0afde503b 100644 --- a/.github/workflows/generate-sdk.yaml +++ b/.github/workflows/generate-sdk.yaml @@ -1,6 +1,6 @@ name: Generate SDK -# Regenerates the OpenAPI document and the SDK, and commits both to the branch. +# Regenerates the OpenAPI document and the SDKs, and commits them to the branch. # Fork PRs cannot receive that commit; main regenerates after merge. on: # Manual regen on main if the post-merge push job missed or failed. @@ -14,6 +14,7 @@ on: - 'packages/trueforge-core/src/**' - '.github/fern/**' - '.github/workflows/generate-sdk.yaml' + - 'scripts/generate-sdk.sh' pull_request: types: [opened, synchronize, reopened] paths: @@ -22,6 +23,7 @@ on: - 'packages/trueforge-core/src/**' - '.github/fern/**' - '.github/workflows/generate-sdk.yaml' + - 'scripts/generate-sdk.sh' concurrency: group: generate-sdk-${{ github.head_ref || github.ref_name }} @@ -32,7 +34,7 @@ permissions: jobs: generate: - name: Generate OpenAPI and TypeScript SDK + name: Generate OpenAPI and SDKs runs-on: ubuntu-latest timeout-minutes: 30 # A pull_request path filter matches the whole PR, not just the new commit, @@ -110,7 +112,7 @@ jobs: elif ! git rev-parse --verify --quiet "${BASE_SHA}^{commit}" >/dev/null; then # A branch's first push reports an all-zero base commit. reason="there is no base commit to compare against" - elif ! git diff --quiet "$BASE_SHA" HEAD -- .github/fern/generators.yml .github/fern/fern.config.json; then + elif ! git diff --quiet "$BASE_SHA" HEAD -- .github/fern/generators.yml .github/fern/fern.config.json scripts/generate-sdk.sh; then reason="the generator config changed" fi @@ -124,7 +126,7 @@ jobs: # Same entrypoint as local `pnpm sdk:generate` (re-runs openapi:write; idempotent). # App-token success = we can push; forks skip Fern so a flake cannot block the PR. - - name: Generate OpenAPI document and TypeScript SDK + - name: Generate OpenAPI document and SDKs if: steps.decide.outputs.needed == 'true' && steps.app-token.outcome == 'success' run: pnpm sdk:generate @@ -141,8 +143,8 @@ jobs: # --porcelain, not `git diff`, so new files count too. # docs/openapi.json is a second copy of the same document (Mintlify); keep it # in the same commit as .github/fern/openapi or the docs site drifts. - if [ -z "$(git status --porcelain -- .github/fern/openapi docs/openapi.json packages/trueforge-sdk pnpm-lock.yaml)" ]; then - echo "OpenAPI document and SDK are already up to date." + if [ -z "$(git status --porcelain -- .github/fern/openapi docs/openapi.json packages/trueforge-sdk python/trueforge_sdk pnpm-lock.yaml)" ]; then + echo "OpenAPI document and SDKs are already up to date." exit 0 fi @@ -152,12 +154,13 @@ jobs: # Bot-authored sdk diffs need a changeset; the PR author never touches this commit. # Skip packages/trueforge-sdk/.fern — Fern metadata only, not a consumer-facing change. # changeset:sdk-regen is a no-op if the sdk is already named in a pending file. + # Python is not Changesets-published yet; do not add it here. if [ -n "$(git status --porcelain -- packages/trueforge-sdk ':!packages/trueforge-sdk/.fern')" ]; then pnpm changeset:sdk-regen fi - git add .github/fern/openapi docs/openapi.json packages/trueforge-sdk pnpm-lock.yaml .changeset - git commit -m "Regenerate OpenAPI document and TypeScript SDK" + git add .github/fern/openapi docs/openapi.json packages/trueforge-sdk python/trueforge_sdk pnpm-lock.yaml .changeset + git commit -m "Regenerate OpenAPI document and SDKs" git push origin "HEAD:refs/heads/$BRANCH" - name: Report skipped commit on fork @@ -168,7 +171,7 @@ jobs: echo "### Fork pull request" echo echo "OpenAPI/SDK commits are skipped on forks (GitHub Apps cannot push to the head repo)." - echo "Do not hand-edit \`packages/trueforge-sdk\` or the OpenAPI documents." + echo "Do not hand-edit \`packages/trueforge-sdk\`, \`python/trueforge_sdk\`, or the OpenAPI documents." echo "After merge, **Generate SDK** on \`main\` regenerates both (or run it manually from Actions)." } >> "$GITHUB_STEP_SUMMARY" echo "::notice::Fork PR: OpenAPI/SDK will be regenerated on main after merge. Do not hand-edit generated files." diff --git a/.prettierignore b/.prettierignore index a1a6038a6..9ac097918 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ dist/ .changeset/ # Fern-generated: formatted by the generator, not by this repo's config. packages/trueforge-sdk/ +python/trueforge_sdk/ .github/fern/openapi/ # Ignore generated openapi.json in docs docs/openapi.json diff --git a/python/trueforge_sdk/.fern/metadata.json b/python/trueforge_sdk/.fern/metadata.json new file mode 100644 index 000000000..00ac941b2 --- /dev/null +++ b/python/trueforge_sdk/.fern/metadata.json @@ -0,0 +1,29 @@ +{ + "cliVersion": "5.106.1", + "generatorName": "fernapi/fern-python-sdk", + "generatorVersion": "5.29.0", + "generatorConfig": { + "package_name": "trueforge_sdk", + "optional_auth": true, + "stream_abstraction": true, + "use_request_defaults": "all", + "inline_path_params": true, + "pydantic_config": { + "enum_type": "forward_compatible_python_enums", + "frozen": false, + "skip_validation": true + }, + "client": { + "class_name": "BaseTrueForge", + "filename": "base_client.py", + "exported_class_name": "TrueForge", + "exported_filename": "client.py" + }, + "pyproject_python_version": ">=3.10" + }, + "originGitCommit": "0cb933cebf8021c09e9842c852246c6d7f342919", + "originGitCommitIsDirty": false, + "invokedBy": "ci", + "requestedVersion": "0.1.0-rc.1", + "ciProvider": "github" +} \ No newline at end of file diff --git a/python/trueforge_sdk/CONTRIBUTING.md b/python/trueforge_sdk/CONTRIBUTING.md new file mode 100644 index 000000000..af948ce72 --- /dev/null +++ b/python/trueforge_sdk/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Python 3.9+ +- pip +- poetry + +### Installation + +Install the project dependencies: + +```bash +poetry install +``` + +### Building + +Build the project: + +```bash +poetry build +``` + +### Testing + +Run the test suite: + +```bash +poetry run pytest +``` + +### Linting and Formatting + +Check code style: + +```bash +poetry run ruff check . +poetry run ruff format . +``` + +### Type Checking + +Run the type checker: + +```bash +poetry run mypy . +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most Python files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The Python SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/python-v2/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `poetry run pytest` +4. Run linting and formatting: `poetry run ruff check .` and `poetry run ruff format .` +5. Run type checking: `poetry run mypy .` +6. Build the project: `poetry build` +7. Commit your changes with a clear commit message +8. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting and linting. Run `poetry run ruff format .` and `poetry run ruff check .` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/python/trueforge_sdk/README.md b/python/trueforge_sdk/README.md new file mode 100644 index 000000000..31ff6250b --- /dev/null +++ b/python/trueforge_sdk/README.md @@ -0,0 +1,226 @@ +# TrueForge Python Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Truefoundry%2FPython) +[![pypi](https://img.shields.io/pypi/v/trueforge_sdk)](https://pypi.python.org/pypi/trueforge_sdk) + +TypeScript client for the TrueForge Agent Harness Server: a self-hosted runtime that executes agent turns and streams them over Server-Sent Events. + +When auth is enabled on the server, pass an ID token via the optional `token` client option (`Authorization: Bearer`). + + +## Table of Contents + +- [Documentation](#documentation) +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Async Client](#async-client) +- [Exception Handling](#exception-handling) +- [Streaming](#streaming) +- [Pagination](#pagination) +- [Advanced](#advanced) + - [Access Raw Response Data](#access-raw-response-data) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Custom Client](#custom-client) +- [Contributing](#contributing) + +## Documentation + +API reference documentation is available [here](https://trueforge.dev/api/use-agent). + +## Installation + +```sh +pip install trueforge_sdk +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create_turn_stream( + session_id="session_id", +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client). + +```python +import asyncio + +from trueforge_sdk import AsyncTrueForge + +client = AsyncTrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + + +async def main() -> None: + async for chunk in client.sessions.create_turn_stream(...): + print(chunk) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from trueforge_sdk.core.api_error import ApiError + +try: + client.sessions.create_turn_stream(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` + +## Streaming + +The SDK supports streaming responses, as well, the response will be a generator that you can loop over. + +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create_turn_stream( + session_id="session_id", +) +``` + +## Pagination + +Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object. + +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.agents.list() +``` + +```python +# You can also iterate through pages and access the typed response per page +pager = client.agents.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 + +The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. +The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. + +```python +from trueforge_sdk import TrueForge + +client = TrueForge(...) +response = client.sessions.with_raw_response.create_turn_stream(...) +print(response.headers) # access the response headers +print(response.status_code) # access the response status code +print(response.data) # access the underlying object +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +Which status codes are retried depends on the `retryStatusCodes` generator configuration: + +**`legacy`** (current default): retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) + +**`recommended`**: retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `max_retries` request option to configure this behavior. + +```python +client.sessions.create_turn_stream(..., request_options={ + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python +from trueforge_sdk import TrueForge + +client = TrueForge(..., timeout=20.0) + +# Override timeout for a specific method +client.sessions.create_turn_stream(..., request_options={ + "timeout": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. + +```python +import httpx +from trueforge_sdk import TrueForge + +client = TrueForge( + ..., + httpx_client=httpx.Client( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/python/trueforge_sdk/pyproject.toml b/python/trueforge_sdk/pyproject.toml new file mode 100644 index 000000000..5ecfe4663 --- /dev/null +++ b/python/trueforge_sdk/pyproject.toml @@ -0,0 +1,94 @@ +[project] +name = "trueforge-sdk" +dynamic = ["version"] + +[tool.poetry] +name = "trueforge-sdk" +version = "0.1.0-rc.1" +description = "" +readme = "README.md" +authors = [] +keywords = [] + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" +] +packages = [ + { include = "trueforge_sdk", from = "src"} +] + +[tool.poetry.dependencies] +python = ">=3.10" +aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"} +httpx = ">=0.21.2" +httpx-aiohttp = { version = "^0.1.8", optional = true, python = ">=3.10"} +pydantic = ">= 1.9.2" +pydantic-core = ">=2.18.2,<3.0.0" +typing_extensions = ">= 4.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = "==1.13.0" +pytest = "^9.0.3" +pytest-asyncio = "^1.0.0" +pytest-xdist = "^3.6.1" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +urllib3 = ">=2.6.3,<3.0.0" +ruff = "==0.11.5" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" +norecursedirs = [ "src" ] +markers = [ + "aiohttp: tests that require httpx_aiohttp to be installed", +] + +[tool.mypy] +plugins = ["pydantic.mypy"] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort +] +ignore = [ + "E402", # Module level import not at top of file + "E501", # Line too long + "E711", # Comparison to `None` should be `cond is not None` + "E712", # Avoid equality comparisons to `True`; use `if ...:` checks + "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks + "E722", # Do not use bare `except` + "E731", # Do not assign a `lambda` expression, use a `def` + "F821", # Undefined name + "F841" # Local variable ... is assigned to but never used +] + +[tool.ruff.lint.isort] +section-order = ["future", "standard-library", "third-party", "first-party"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.extras] +aiohttp=["aiohttp", "httpx-aiohttp"] diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md new file mode 100644 index 000000000..0f8caedb6 --- /dev/null +++ b/python/trueforge_sdk/reference.md @@ -0,0 +1,4684 @@ +# Reference +## Internal +
client.internal.list_permissions(...) -> ListPermissionsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Return granted actions for each requested agent, schedule, or session id. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, PermissionResourceType + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.list_permissions( + resource_ids=[ + "resource_ids" + ], + resource_type=PermissionResourceType.AGENT, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**resource_ids:** `typing.List[str]` — Resource ids of `resource_type` to evaluate for the caller. + +
+
+ +
+
+ +**resource_type:** `PermissionResourceType` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Agents +
client.agents.list(...) -> ListAgentsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List configured agents for the tenant, ordered by name. Optional `agent_name` filters by substring. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.agents.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 50, max 100. + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque token from a previous response `next_page_token`. + +
+
+ +
+
+ +**agent_name:** `typing.Optional[str]` — Case-insensitive substring match on agent name. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.agents.create(...) -> GetAgentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates an agent and allocates an immutable id. Fails if `name` is already taken. Name cannot be changed later. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, AgentSpec, Model + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.agents.create( + description="description", + manifest=AgentSpec( + model=Model( + name="name", + ), + ), + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**description:** `str` + +
+
+ +
+
+ +**manifest:** `AgentSpec` + +
+
+ +
+
+ +**name:** `ResourceName` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.agents.get(...) -> GetAgentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Fetch a configured agent by immutable id. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.agents.get( + agent_id="agent_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_id:** `str` — Immutable agent identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.agents.update(...) -> GetAgentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Update an existing agent by immutable id. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, AgentSpec, Model + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.agents.update( + agent_id="agent_id", + manifest=AgentSpec( + model=Model( + name="name", + ), + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_id:** `str` — Immutable agent identifier. + +
+
+ +
+
+ +**manifest:** `AgentSpec` + +
+
+ +
+
+ +**description:** `typing.Optional[str]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.agents.delete(...) -> DeleteAgentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Delete a configured agent by immutable id. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.agents.delete( + agent_id="agent_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_id:** `str` — Immutable agent identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Auth +
client.auth.me() -> GetMeResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns the authenticated caller identity (`type`, `tenant_id`, `subject`, `roles`) wrapped as `{ data }`. `type` is `oidc-connected` when browser OIDC is enabled, otherwise `default`. When auth is enabled this requires a valid `id_token` cookie or `Authorization: Bearer` token (401 otherwise). When auth is disabled, returns the standalone default identity. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.auth.me() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Server +
client.server.get_capabilities() -> GetCapabilitiesResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Report optional runtime capabilities available for this tenant. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.server.get_capabilities() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## MCP Servers +
client.mcp_servers.list() -> ListAvailableMcpServersResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Configured MCP servers as a slim name/url list for the composer. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.mcp_servers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.mcp_servers.get(...) -> GetAvailableMcpServerResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +A single MCP server as the slim chat projection, with live per-user auth_status. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.mcp_servers.get( + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` — MCP server name. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.mcp_servers.authorize(...) -> McpAuthStatus +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns current auth status. When OAuth is required, includes an authorization URL. Optional return_to is the post-consent landing path. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.mcp_servers.authorize( + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` — MCP server name. + +
+
+ +
+
+ +**return_to:** `typing.Optional[str]` — Same-origin path to land in the browser after consent. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.mcp_servers.delete_authorization(...) -> GetMcpServerResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Disconnects OAuth for the MCP server when applicable and returns the updated server with auth_status. No-op when the server does not use stored OAuth tokens. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.mcp_servers.delete_authorization( + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` — MCP server name. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.mcp_servers.list_tools(...) -> ListMcpServerToolsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +All tools exposed by the given MCP server (non-paginated), as returned by the MCP `tools/list` call. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.mcp_servers.list_tools( + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` — MCP server name. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Models +
client.models.list() -> ListAvailableModelsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Configured models as a slim FQN list for the composer. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.models.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Schedules +
client.schedules.list(...) -> ListSchedulesResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List schedules for the tenant, newest first. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 25 + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque token from a previous response `next_page_token`. + +
+
+ +
+
+ +**agent_names:** `typing.Optional[str]` — Filter by one or more agent names (comma-separated). When set, at least one name is required. + +
+
+ +
+
+ +**created_by_me:** `typing.Optional[bool]` — When true, only schedules created by the authenticated subject. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.schedules.create(...) -> GetScheduleResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a schedule for an existing agent (by name) and add its first pending run when active. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, ScheduleManifest + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.create( + agent_name="agent_name", + manifest=ScheduleManifest( + cron="cron", + task="task", + ), + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_name:** `ResourceName` + +
+
+ +
+
+ +**manifest:** `ScheduleManifest` + +
+
+ +
+
+ +**name:** `ResourceName` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.schedules.create_run(...) -> CreateScheduleRunResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Start a schedule run immediately using the schedule task. Does not replace or advance the cron pending run. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.create_run( + schedule_id="schedule_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**schedule_id:** `str` — Immutable schedule identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.schedules.get(...) -> GetScheduleResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get a schedule by id. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.get( + schedule_id="schedule_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**schedule_id:** `str` — Immutable schedule identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.schedules.update(...) -> GetScheduleResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Replace name and manifest; replaces or drops the pending run when status/cron/timezone change. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, ScheduleManifest + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.update( + schedule_id="schedule_id", + manifest=ScheduleManifest( + cron="cron", + task="task", + ), + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**schedule_id:** `str` — Immutable schedule identifier. + +
+
+ +
+
+ +**manifest:** `ScheduleManifest` + +
+
+ +
+
+ +**name:** `ResourceName` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.schedules.delete(...) -> DeleteScheduleResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Delete a schedule and its runs. Idempotent. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.delete( + schedule_id="schedule_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**schedule_id:** `str` — Immutable schedule identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.schedules.list_runs(...) -> ListScheduleRunsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List runs of a schedule, newest `scheduled_for` first. Available to its creator or a manager of its agent. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.schedules.list_runs( + schedule_id="schedule_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**schedule_id:** `str` — Immutable schedule identifier. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 25 + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque token from a previous response `next_page_token`. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Sessions +
client.sessions.list(...) -> ListSessionsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List the sessions (newest first by default). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 25, max 25. + +
+
+ +
+
+ +**order:** `typing.Optional[ListSessionsOrder]` — Sort sessions by `updated_at`. Defaults to "desc". + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque keyset cursor from a previous response `next_page_token`. + +
+
+ +
+
+ +**start_timestamp:** `typing.Optional[datetime.datetime]` — Inclusive lower bound on `created_at` (ISO-8601 / RFC 3339). + +
+
+ +
+
+ +**end_timestamp:** `typing.Optional[datetime.datetime]` — Inclusive upper bound on `created_at` (ISO-8601 / RFC 3339). + +
+
+ +
+
+ +**agent_id:** `typing.Optional[str]` — When set, only sessions bound to this agent id are returned. + +
+
+ +
+
+ +**created_by_me:** `typing.Optional[bool]` — When true, only sessions created by the authenticated subject. + +
+
+ +
+
+ +**metadata:** `typing.Optional[SessionMetadata]` — Exact metadata pairs as metadata[key]=value. Sessions must contain all pairs. + +
+
+ +
+
+ +**source_type:** `typing.Optional[SessionSourceType]` — When set, returns only sessions created by this source type. + +
+
+ +
+
+ +**source_id:** `typing.Optional[str]` — When set, returns only sessions from this specific source. Requires source_type. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create(...) -> GetSessionResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a session with `agent` as either `{ name }` (named registry binding) or `{ spec: AgentSpec }` (inline). Named sessions snapshot the agent name at create and resolve the live agent on each turn. Responses use `{ type: "reference", name, id }` or `{ type: "inline", spec }`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, SessionAgentNameRef + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create( + agent=SessionAgentNameRef( + name="name", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent:** `CreateSessionAgent` + +
+
+ +
+
+ +**metadata:** `typing.Optional[SessionMetadata]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.get(...) -> GetSessionResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Fetch a session by ID. Only the session creator may fetch it. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.get( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.delete(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Delete a session and all related turns, events, and internal state. Only the session creator may delete it. Idempotent if already gone. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.delete( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.update(...) -> GetSessionResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Update a session by replacing `agent` with `{ spec: AgentSpec }`. Named (reference) sessions reject agent updates. An empty body is a valid no-op that refreshes `updated_at`. Only the session creator may update it. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.update( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**agent:** `typing.Optional[SessionAgentSpecBody]` + +
+
+ +
+
+ +**metadata:** `typing.Optional[SessionMetadata]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.cancel(...) -> CancelSessionResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Cancel the running last turn for a session. Only the session creator may cancel. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.cancel( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.list_events(...) -> ListSessionEventsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List session events as `{ turn_id, event }` across the active turn branch (newest first), including persisted events from a running tip. Each turn contributes turn.created, content events (model.message, tool.call, …), and turn.done when terminal; streaming deltas are not included. Use `page_token` to paginate backward toward older events while retaining the original branch anchor. Only the session creator may list events. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.list_events( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Pagination cursor from `pagination.next_page_token`. It retains the branch anchor turn and returns older events toward the session start. + +
+
+ +
+
+ +**last_turn_id:** `typing.Optional[str]` — Newest turn in the listing window (initial load only; ignored when `page_token` is set). Lists that turn and its ancestors, newest events first. Omit to use the session last turn. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 100, max 100. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.list_turns(...) -> ListTurnsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List turns for a session (newest first by default), token-paginated. Only the session creator may list turns. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.list_turns( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 25, max 25. + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque token from a previous response `next_page_token`. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create_turn_stream(...) -> typing.Iterator[bytes] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a turn within a session and execute it. +Only the session creator may create turns. +When `stream` is true (default), respond with a Server-Sent Events stream of turn events. +When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. +Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create_turn_stream( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**stream:** `typing.Literal` — When true (default), stream turn events as SSE. When false, return the running turn immediately. + +
+
+ +
+
+ +**input:** `typing.Optional[typing.List[TurnInputItem]]` — Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + +
+
+ +
+
+ +**previous_turn_id:** `typing.Optional[PreviousTurnIdInput]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create_turn(...) -> GetTurnResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a turn within a session and execute it. +Only the session creator may create turns. +When `stream` is true (default), respond with a Server-Sent Events stream of turn events. +When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. +Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create_turn_stream( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**stream:** `typing.Literal` — When true (default), stream turn events as SSE. When false, return the running turn immediately. + +
+
+ +
+
+ +**input:** `typing.Optional[typing.List[TurnInputItem]]` — Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + +
+
+ +
+
+ +**previous_turn_id:** `typing.Optional[PreviousTurnIdInput]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.get_turn(...) -> GetTurnResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Fetch a single turn by ID. Only the session creator may fetch it. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.get_turn( + session_id="session_id", + turn_id="turn_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `str` — Turn identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.download_sandbox_file(...) -> typing.Iterator[bytes] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Download a file from the sandbox this turn ran in. Paths come from the assistant's `sandbox_artifacts` block. Only the session creator may download. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.download_sandbox_file( + session_id="session_id", + turn_id="turn_id", + path="x", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `str` — Turn identifier. + +
+
+ +
+
+ +**path:** `str` — Absolute or sandbox-working-directory-relative file path, as listed in the assistant's `sandbox_artifacts` block. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.list_turn_events(...) -> ListTurnEventsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Paginated persisted events for a turn (insertion order by default). Only the session creator may list events. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.list_turn_events( + session_id="session_id", + turn_id="turn_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `str` — Turn identifier. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 100, max 100. + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque token from a previous response `next_page_token`. + +
+
+ +
+
+ +**order:** `typing.Optional[ListTurnEventsOrder]` — Sort events by insertion order. Defaults to "asc". + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.subscribe_to_turn(...) -> typing.Iterator[bytes] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.subscribe_to_turn( + session_id="session_id", + turn_id="turn_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `str` — Turn identifier. + +
+
+ +
+
+ +**after_sequence_number:** `typing.Optional[int]` — Exclusive resume cursor: replay only events with a sequence number greater than this value. Omit to start from the beginning of the live buffer. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Skills +
client.skills.list() -> ListAvailableSkillsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Configured skills as a slim name/description list for the composer. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.skills.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.skills.list_versions(...) -> ListSkillVersionsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Versions for one skill. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.skills.list_versions( + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` — Skill name. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Catalogs McpServers +
client.catalogs.mcp_servers.list() -> GetMcpServerCatalogResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shipped MCP server presets (discovery-only). Copy into PUT /settings/mcp-servers to configure. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.catalogs.mcp_servers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Catalogs ModelProviders +
client.catalogs.model_providers.list() -> GetModelProviderCatalogResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shipped model-provider presets (discovery-only). Copy into PUT /settings/model-providers to configure. Includes a `custom` sentinel with `supported_reasoning_efforts`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.catalogs.model_providers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Catalogs SandboxProviders +
client.catalogs.sandbox_providers.list() -> GetSandboxProviderCatalogResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shipped sandbox-provider presets (discovery-only). Copy into PUT /settings/sandbox-providers to configure. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.catalogs.sandbox_providers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Catalogs Skills +
client.catalogs.skills.list() -> GetSkillCatalogResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shipped skill presets (discovery-only). Copy into PUT /settings/skills to configure. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.catalogs.skills.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Internal Metrics +
client.internal.metrics.list_charts() -> GetSessionMetricsChartResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List available session metric charts. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.metrics.list_charts() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.internal.metrics.get_chart_data(...) -> GetSessionMetricsChartDataResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Return one chart for the caller's sessions on a named agent over an inclusive creation-time window. Uses hourly buckets for windows up to 24 hours and daily UTC buckets otherwise. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, SessionMetricsChartName +import datetime + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.metrics.get_chart_data( + agent_id="agent_id", + start_timestamp=datetime.datetime.fromisoformat("2024-01-15T09:30:00+00:00"), + end_timestamp=datetime.datetime.fromisoformat("2024-01-15T09:30:00+00:00"), + chart_name=SessionMetricsChartName.SESSIONS_OVER_TIME, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_id:** `str` — Named agent identifier. + +
+
+ +
+
+ +**start_timestamp:** `datetime.datetime` — Inclusive lower bound on session `created_at`. + +
+
+ +
+
+ +**end_timestamp:** `datetime.datetime` — Inclusive upper bound on session `created_at`. + +
+
+ +
+
+ +**chart_name:** `SessionMetricsChartName` — Session metrics chart to return. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.internal.metrics.get_meters(...) -> GetSessionMetricsMeterResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Aggregate the caller's session meters for a named agent over an inclusive creation-time window. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge +import datetime + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.metrics.get_meters( + agent_id="agent_id", + start_timestamp=datetime.datetime.fromisoformat("2024-01-15T09:30:00+00:00"), + end_timestamp=datetime.datetime.fromisoformat("2024-01-15T09:30:00+00:00"), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_id:** `str` — Named agent identifier. + +
+
+ +
+
+ +**start_timestamp:** `datetime.datetime` — Inclusive lower bound on session `created_at`. + +
+
+ +
+
+ +**end_timestamp:** `datetime.datetime` — Inclusive upper bound on session `created_at`. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Internal Schedules +
client.internal.schedules.execute_run(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Execute a persisted schedule run using its saved schedule and agent. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.schedules.execute_run( + schedule_run_id="schedule_run_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**schedule_run_id:** `str` — Immutable schedule run identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Internal Sessions +
client.internal.sessions.get_or_create_by_external_id(...) -> GetSessionResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Idempotent get-or-create: returns the existing session for this `external_id`, or creates one +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, SessionAgentNameRef + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.sessions.get_or_create_by_external_id( + agent=SessionAgentNameRef( + name="name", + ), + external_id="external_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent:** `CreateSessionAgent` + +
+
+ +
+
+ +**external_id:** `str` — Caller-supplied id unique within the tenant. + +
+
+ +
+
+ +**source:** `typing.Optional[SessionSourceSchedule]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Internal Agents +
client.internal.agents.get_code_snippets(...) -> GetAgentCodeSnippetsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +TypeScript TrueForge SDK samples (stream and non-stream) for creating a session and turn against this agent. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.internal.agents.get_code_snippets( + agent_id="agent_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**agent_id:** `str` — Immutable agent identifier. + +
+
+ +
+
+ +**base_url:** `typing.Optional[str]` — Public SDK base URL from the browser. When omitted, derived from the request origin and PUBLIC_BASE_URL. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Settings McpServers +
client.settings.mcp_servers.list() -> ListMcpServersResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Configured MCP servers with auth_status. Header secrets are redacted. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.mcp_servers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.mcp_servers.create(...) -> GetMcpServerResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates an MCP server by `name`. Fails if `name` is already taken. Runs DCR registration when `auth.type` is `dcr`. Header secrets: real value required; redacted with no stored value returns 400. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, RemoteMcpServerManifest + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.mcp_servers.create( + manifest=RemoteMcpServerManifest( + description="description", + name="name", + type="remote", + url="url", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `McpServerManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.mcp_servers.create_or_update(...) -> GetMcpServerResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create or replace by `name`. Header secrets: real value sets/rotates; redacted keeps existing (400 if none). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, RemoteMcpServerManifest + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.mcp_servers.create_or_update( + manifest=RemoteMcpServerManifest( + description="description", + name="name", + type="remote", + url="url", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `McpServerManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.mcp_servers.get(...) -> GetMcpServerResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +A single MCP server by name, with nested live auth_status (settings / admin projection). Header auth values are redacted. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.mcp_servers.get( + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `str` — MCP server name. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Settings ModelProviders +
client.settings.model_providers.list() -> ListModelProvidersResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +All configured providers with nested manifests. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.model_providers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.model_providers.create(...) -> GetModelProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates a provider (models included). Fails if `name` is already taken. Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value required; redacted with no stored secret returns 400. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, AlibabaModelProvider, ModelProviderAuth, ConfiguredModel, ModelProperties + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.model_providers.create( + manifest=AlibabaModelProvider( + auth=ModelProviderAuth( + api_key="api_key", + ), + models=[ + ConfiguredModel( + model_id="model_id", + name="name", + properties=ModelProperties(), + ) + ], + type="alibaba", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `ModelProviderManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.model_providers.create_or_update(...) -> GetModelProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create or replace a provider (models included). Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, AlibabaModelProvider, ModelProviderAuth, ConfiguredModel, ModelProperties + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.model_providers.create_or_update( + manifest=AlibabaModelProvider( + auth=ModelProviderAuth( + api_key="api_key", + ), + models=[ + ConfiguredModel( + model_id="model_id", + name="name", + properties=ModelProperties(), + ) + ], + type="alibaba", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `ModelProviderManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Settings SandboxProviders +
client.settings.sandbox_providers.get() -> GetSandboxProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +The single configured sandbox provider for this tenant. `auth.api_key` is redacted. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.sandbox_providers.get() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.sandbox_providers.create_or_update(...) -> GetSandboxProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, SandboxProviderManifest, DaytonaSandboxProviderAuth + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.sandbox_providers.create_or_update( + manifest=SandboxProviderManifest( + auth=DaytonaSandboxProviderAuth( + api_key="api_key", + ), + auto_archive_interval_in_minutes=1, + auto_delete_interval_in_minutes=1, + auto_stop_interval_in_minutes=1, + exec_timeout_ms=1, + type="daytona", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `SandboxProviderManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Settings Skills +
client.settings.skills.list() -> ListSkillsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +All configured skills with nested manifests (settings / admin projection). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.skills.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.skills.create(...) -> GetSkillResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates a skill keyed by `name`. Fails if `name` is already taken. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, GitSkill + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.skills.create( + manifest=GitSkill( + description="description", + name="name", + ref="ref", + type="git", + url="url", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `SkillManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.skills.create_or_update(...) -> GetSkillResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Full upsert keyed by `name`: creates the skill or replaces its entire manifest. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, GitSkill + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.skills.create_or_update( + manifest=GitSkill( + description="description", + name="name", + ref="ref", + type="git", + url="url", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `SkillManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/python/trueforge_sdk/requirements.txt b/python/trueforge_sdk/requirements.txt new file mode 100644 index 000000000..443b1af40 --- /dev/null +++ b/python/trueforge_sdk/requirements.txt @@ -0,0 +1,4 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2,<3.0.0 +typing_extensions>= 4.0.0 diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py new file mode 100644 index 000000000..47d8597df --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -0,0 +1,798 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + ActionRequiredEvent, + Agent, + AgentCodeSnippet, + AgentCodeSnippetSampleCode, + AgentCodeSnippets, + AgentInfo, + AgentParent, + AgentSpec, + AlibabaModelProvider, + AnthropicModelProvider, + ApprovalAllow, + ApprovalDecision, + ApprovalDeny, + AskUserQuestionsConfig, + AvailableMcpServer, + AvailableModel, + AvailableModelProvider, + AvailableSkill, + BaseMcpAuthRequiredEvent, + BaseThreadDoneEvent, + CancelSessionResponse, + CapabilitiesData, + CatalogCustomModelProvider, + CatalogMcpServer, + CatalogMcpServerType, + CatalogModel, + CatalogModelProvider, + CatalogSandboxProvider, + CatalogSkill, + CatalogSkillType, + CatalogWellKnownModelProvider, + CatalogWellKnownModelProviderType, + ChatCompletionChunkDeltaToolCall, + ChatCompletionChunkDeltaToolCallFunction, + ChatCompletionContentPartRefusal, + ChatCompletionContentPartText, + ChatCompletionMessageToolCall, + ChatCompletionMessageToolCallFunction, + CompactionConfig, + ConfiguredMcpServer, + ConfiguredModel, + ConfiguredModelProvider, + ConfiguredSandboxProvider, + ConfiguredSkill, + ContextManagementConfig, + CreateScheduleRunResponse, + CreateSessionAgent, + CreatedBySubject, + CronExpression, + CustomModelProvider, + DaytonaSandboxProviderAuth, + DeleteAgentResponse, + DeleteScheduleResponse, + DynamicSubAgentsConfig, + ExtendedChunkDeltaToolCall, + FileContent, + FinishReason, + FireworksModelProvider, + GenerativeUiConfig, + GetAgentCodeSnippetsResponse, + GetAgentResponse, + GetAvailableMcpServerResponse, + GetCapabilitiesResponse, + GetMcpServerCatalogResponse, + GetMcpServerResponse, + GetMeResponse, + GetMeSubject, + GetModelProviderCatalogResponse, + GetModelProviderResponse, + GetSandboxProviderCatalogResponse, + GetSandboxProviderResponse, + GetScheduleResponse, + GetSessionMetricsChartDataResponse, + GetSessionMetricsChartResponse, + GetSessionMetricsMeterResponse, + GetSessionResponse, + GetSkillCatalogResponse, + GetSkillResponse, + GetTurnResponse, + GitSkill, + GoogleGeminiModelProvider, + InitialUserMessage, + InputTokensCompactionTrigger, + LargeToolResponseConfig, + ListAgentsResponse, + ListAvailableMcpServersResponse, + ListAvailableModelsResponse, + ListAvailableSkillsResponse, + ListMcpServerToolsResponse, + ListMcpServersResponse, + ListModelProvidersResponse, + ListPermissionsResponse, + ListScheduleRunsResponse, + ListSchedulesResponse, + ListSessionEventsResponse, + ListSessionsOrder, + ListSessionsResponse, + ListSkillVersionsResponse, + ListSkillsResponse, + ListTurnEventsOrder, + ListTurnEventsResponse, + ListTurnsResponse, + McpAuthRequiredEvent, + McpAuthStatus, + McpAuthStatusStatus, + McpInitializeEvent, + McpServer, + McpServerApprovalToolSelector, + McpServerAuthInfo, + McpServerAuthPublic, + McpServerAuthPublicOne, + McpServerAuthPublicZero, + McpServerDcrAuth, + McpServerHeaderAuth, + McpServerInitInfo, + McpServerInitInfoTransportType, + McpServerManifest, + McpServerManifestAuth, + McpServerToolSelector, + McpToolInfo, + Me, + MeSessionType, + MetricsUnit, + Model, + ModelMessageDeltaEvent, + ModelMessageEvent, + ModelMessageEventContent, + ModelMessageEventContentOneItem, + ModelMessageUsage, + ModelMessageUsageInputTokensBreakdown, + ModelParams, + ModelProperties, + ModelProviderAuth, + ModelProviderManifest, + MoonshotModelProvider, + OpenAiModelProvider, + PermissionResourceType, + PreviousTurnIdInput, + RawToolCall, + ReasoningEffort, + RemoteMcpServerManifest, + RequestErrorResponse, + RequestErrorResponseError, + ResourceName, + ResourcePermission, + ResponseFormat, + ResponseFormatJsonObject, + ResponseFormatJsonSchema, + ResponseFormatJsonSchemaJsonSchema, + ResponseFormatText, + RuntimeConfig, + SandboxBuildStatus, + SandboxCapability, + SandboxConfig, + SandboxCreatedEvent, + SandboxProviderManifest, + Schedule, + ScheduleManifest, + ScheduleRun, + ScheduleRunStatus, + ScheduleStatus, + Session, + SessionAgent, + SessionAgentInline, + SessionAgentNameRef, + SessionAgentReference, + SessionAgentSpecBody, + SessionEvent, + SessionEventItem, + SessionMetadata, + SessionMetrics, + SessionMetricsChart, + SessionMetricsChartDataResponse, + SessionMetricsChartName, + SessionMetricsChartResponse, + SessionMetricsGraph, + SessionMetricsGraphLine, + SessionMetricsMeter, + SessionMetricsMeterName, + SessionMetricsMeterResponse, + SessionMetricsPoint, + SessionSource, + SessionSourceSchedule, + SessionSourceType, + SettingsCapability, + Skill, + SkillCapability, + SkillManifest, + SkillVersion, + TextContent, + ThreadCreatedEvent, + ThreadDoneEvent, + ThreadState, + ThreadStateDone, + ThreadStateError, + Timezone, + TogetherAiModelProvider, + TokenPagination, + ToolApprovalRequiredEvent, + ToolCall, + ToolCallRef, + ToolInfo, + ToolResponseEvent, + ToolResponseRequiredEvent, + TrueFoundryMcpServerManifest, + TrueFoundryModelProvider, + TrueFoundryRegistrySkill, + TrueFoundrySystemToolInfo, + Turn, + TurnCreatedEvent, + TurnDoneEvent, + TurnDoneEventState, + TurnInputItem, + TurnMetrics, + TurnState, + TurnStateCancelled, + TurnStateCancelledMetrics, + TurnStateCancelledReason, + TurnStateDone, + TurnStateError, + TurnStateErrorMetrics, + TurnStateRunning, + TurnStreamingEvent, + UserMessage, + UserMessageContent, + UserMessageContentItem, + UserToolApprovalEvent, + UserToolResponseEvent, + ZaiModelProvider, + ) + from .errors import ( + BadGatewayError, + BadRequestError, + ConflictError, + ContentTooLargeError, + FailedDependencyError, + ForbiddenError, + GoneError, + InternalServerError, + NotFoundError, + PreconditionFailedError, + UnauthorizedError, + UnprocessableEntityError, + ) + from . import agents, auth, catalogs, internal, mcp_servers, models, schedules, server, sessions, settings, skills + from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient + from .client import AsyncTrueForge, TrueForge + from .version import __version__ +_dynamic_imports: typing.Dict[str, str] = { + "ActionRequiredEvent": ".types", + "Agent": ".types", + "AgentCodeSnippet": ".types", + "AgentCodeSnippetSampleCode": ".types", + "AgentCodeSnippets": ".types", + "AgentInfo": ".types", + "AgentParent": ".types", + "AgentSpec": ".types", + "AlibabaModelProvider": ".types", + "AnthropicModelProvider": ".types", + "ApprovalAllow": ".types", + "ApprovalDecision": ".types", + "ApprovalDeny": ".types", + "AskUserQuestionsConfig": ".types", + "AsyncTrueForge": ".client", + "AvailableMcpServer": ".types", + "AvailableModel": ".types", + "AvailableModelProvider": ".types", + "AvailableSkill": ".types", + "BadGatewayError": ".errors", + "BadRequestError": ".errors", + "BaseMcpAuthRequiredEvent": ".types", + "BaseThreadDoneEvent": ".types", + "CancelSessionResponse": ".types", + "CapabilitiesData": ".types", + "CatalogCustomModelProvider": ".types", + "CatalogMcpServer": ".types", + "CatalogMcpServerType": ".types", + "CatalogModel": ".types", + "CatalogModelProvider": ".types", + "CatalogSandboxProvider": ".types", + "CatalogSkill": ".types", + "CatalogSkillType": ".types", + "CatalogWellKnownModelProvider": ".types", + "CatalogWellKnownModelProviderType": ".types", + "ChatCompletionChunkDeltaToolCall": ".types", + "ChatCompletionChunkDeltaToolCallFunction": ".types", + "ChatCompletionContentPartRefusal": ".types", + "ChatCompletionContentPartText": ".types", + "ChatCompletionMessageToolCall": ".types", + "ChatCompletionMessageToolCallFunction": ".types", + "CompactionConfig": ".types", + "ConfiguredMcpServer": ".types", + "ConfiguredModel": ".types", + "ConfiguredModelProvider": ".types", + "ConfiguredSandboxProvider": ".types", + "ConfiguredSkill": ".types", + "ConflictError": ".errors", + "ContentTooLargeError": ".errors", + "ContextManagementConfig": ".types", + "CreateScheduleRunResponse": ".types", + "CreateSessionAgent": ".types", + "CreatedBySubject": ".types", + "CronExpression": ".types", + "CustomModelProvider": ".types", + "DaytonaSandboxProviderAuth": ".types", + "DefaultAioHttpClient": "._default_clients", + "DefaultAsyncHttpxClient": "._default_clients", + "DeleteAgentResponse": ".types", + "DeleteScheduleResponse": ".types", + "DynamicSubAgentsConfig": ".types", + "ExtendedChunkDeltaToolCall": ".types", + "FailedDependencyError": ".errors", + "FileContent": ".types", + "FinishReason": ".types", + "FireworksModelProvider": ".types", + "ForbiddenError": ".errors", + "GenerativeUiConfig": ".types", + "GetAgentCodeSnippetsResponse": ".types", + "GetAgentResponse": ".types", + "GetAvailableMcpServerResponse": ".types", + "GetCapabilitiesResponse": ".types", + "GetMcpServerCatalogResponse": ".types", + "GetMcpServerResponse": ".types", + "GetMeResponse": ".types", + "GetMeSubject": ".types", + "GetModelProviderCatalogResponse": ".types", + "GetModelProviderResponse": ".types", + "GetSandboxProviderCatalogResponse": ".types", + "GetSandboxProviderResponse": ".types", + "GetScheduleResponse": ".types", + "GetSessionMetricsChartDataResponse": ".types", + "GetSessionMetricsChartResponse": ".types", + "GetSessionMetricsMeterResponse": ".types", + "GetSessionResponse": ".types", + "GetSkillCatalogResponse": ".types", + "GetSkillResponse": ".types", + "GetTurnResponse": ".types", + "GitSkill": ".types", + "GoneError": ".errors", + "GoogleGeminiModelProvider": ".types", + "InitialUserMessage": ".types", + "InputTokensCompactionTrigger": ".types", + "InternalServerError": ".errors", + "LargeToolResponseConfig": ".types", + "ListAgentsResponse": ".types", + "ListAvailableMcpServersResponse": ".types", + "ListAvailableModelsResponse": ".types", + "ListAvailableSkillsResponse": ".types", + "ListMcpServerToolsResponse": ".types", + "ListMcpServersResponse": ".types", + "ListModelProvidersResponse": ".types", + "ListPermissionsResponse": ".types", + "ListScheduleRunsResponse": ".types", + "ListSchedulesResponse": ".types", + "ListSessionEventsResponse": ".types", + "ListSessionsOrder": ".types", + "ListSessionsResponse": ".types", + "ListSkillVersionsResponse": ".types", + "ListSkillsResponse": ".types", + "ListTurnEventsOrder": ".types", + "ListTurnEventsResponse": ".types", + "ListTurnsResponse": ".types", + "McpAuthRequiredEvent": ".types", + "McpAuthStatus": ".types", + "McpAuthStatusStatus": ".types", + "McpInitializeEvent": ".types", + "McpServer": ".types", + "McpServerApprovalToolSelector": ".types", + "McpServerAuthInfo": ".types", + "McpServerAuthPublic": ".types", + "McpServerAuthPublicOne": ".types", + "McpServerAuthPublicZero": ".types", + "McpServerDcrAuth": ".types", + "McpServerHeaderAuth": ".types", + "McpServerInitInfo": ".types", + "McpServerInitInfoTransportType": ".types", + "McpServerManifest": ".types", + "McpServerManifestAuth": ".types", + "McpServerToolSelector": ".types", + "McpToolInfo": ".types", + "Me": ".types", + "MeSessionType": ".types", + "MetricsUnit": ".types", + "Model": ".types", + "ModelMessageDeltaEvent": ".types", + "ModelMessageEvent": ".types", + "ModelMessageEventContent": ".types", + "ModelMessageEventContentOneItem": ".types", + "ModelMessageUsage": ".types", + "ModelMessageUsageInputTokensBreakdown": ".types", + "ModelParams": ".types", + "ModelProperties": ".types", + "ModelProviderAuth": ".types", + "ModelProviderManifest": ".types", + "MoonshotModelProvider": ".types", + "NotFoundError": ".errors", + "OpenAiModelProvider": ".types", + "PermissionResourceType": ".types", + "PreconditionFailedError": ".errors", + "PreviousTurnIdInput": ".types", + "RawToolCall": ".types", + "ReasoningEffort": ".types", + "RemoteMcpServerManifest": ".types", + "RequestErrorResponse": ".types", + "RequestErrorResponseError": ".types", + "ResourceName": ".types", + "ResourcePermission": ".types", + "ResponseFormat": ".types", + "ResponseFormatJsonObject": ".types", + "ResponseFormatJsonSchema": ".types", + "ResponseFormatJsonSchemaJsonSchema": ".types", + "ResponseFormatText": ".types", + "RuntimeConfig": ".types", + "SandboxBuildStatus": ".types", + "SandboxCapability": ".types", + "SandboxConfig": ".types", + "SandboxCreatedEvent": ".types", + "SandboxProviderManifest": ".types", + "Schedule": ".types", + "ScheduleManifest": ".types", + "ScheduleRun": ".types", + "ScheduleRunStatus": ".types", + "ScheduleStatus": ".types", + "Session": ".types", + "SessionAgent": ".types", + "SessionAgentInline": ".types", + "SessionAgentNameRef": ".types", + "SessionAgentReference": ".types", + "SessionAgentSpecBody": ".types", + "SessionEvent": ".types", + "SessionEventItem": ".types", + "SessionMetadata": ".types", + "SessionMetrics": ".types", + "SessionMetricsChart": ".types", + "SessionMetricsChartDataResponse": ".types", + "SessionMetricsChartName": ".types", + "SessionMetricsChartResponse": ".types", + "SessionMetricsGraph": ".types", + "SessionMetricsGraphLine": ".types", + "SessionMetricsMeter": ".types", + "SessionMetricsMeterName": ".types", + "SessionMetricsMeterResponse": ".types", + "SessionMetricsPoint": ".types", + "SessionSource": ".types", + "SessionSourceSchedule": ".types", + "SessionSourceType": ".types", + "SettingsCapability": ".types", + "Skill": ".types", + "SkillCapability": ".types", + "SkillManifest": ".types", + "SkillVersion": ".types", + "TextContent": ".types", + "ThreadCreatedEvent": ".types", + "ThreadDoneEvent": ".types", + "ThreadState": ".types", + "ThreadStateDone": ".types", + "ThreadStateError": ".types", + "Timezone": ".types", + "TogetherAiModelProvider": ".types", + "TokenPagination": ".types", + "ToolApprovalRequiredEvent": ".types", + "ToolCall": ".types", + "ToolCallRef": ".types", + "ToolInfo": ".types", + "ToolResponseEvent": ".types", + "ToolResponseRequiredEvent": ".types", + "TrueForge": ".client", + "TrueFoundryMcpServerManifest": ".types", + "TrueFoundryModelProvider": ".types", + "TrueFoundryRegistrySkill": ".types", + "TrueFoundrySystemToolInfo": ".types", + "Turn": ".types", + "TurnCreatedEvent": ".types", + "TurnDoneEvent": ".types", + "TurnDoneEventState": ".types", + "TurnInputItem": ".types", + "TurnMetrics": ".types", + "TurnState": ".types", + "TurnStateCancelled": ".types", + "TurnStateCancelledMetrics": ".types", + "TurnStateCancelledReason": ".types", + "TurnStateDone": ".types", + "TurnStateError": ".types", + "TurnStateErrorMetrics": ".types", + "TurnStateRunning": ".types", + "TurnStreamingEvent": ".types", + "UnauthorizedError": ".errors", + "UnprocessableEntityError": ".errors", + "UserMessage": ".types", + "UserMessageContent": ".types", + "UserMessageContentItem": ".types", + "UserToolApprovalEvent": ".types", + "UserToolResponseEvent": ".types", + "ZaiModelProvider": ".types", + "__version__": ".version", + "agents": ".agents", + "auth": ".auth", + "catalogs": ".catalogs", + "internal": ".internal", + "mcp_servers": ".mcp_servers", + "models": ".models", + "schedules": ".schedules", + "server": ".server", + "sessions": ".sessions", + "settings": ".settings", + "skills": ".skills", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ActionRequiredEvent", + "Agent", + "AgentCodeSnippet", + "AgentCodeSnippetSampleCode", + "AgentCodeSnippets", + "AgentInfo", + "AgentParent", + "AgentSpec", + "AlibabaModelProvider", + "AnthropicModelProvider", + "ApprovalAllow", + "ApprovalDecision", + "ApprovalDeny", + "AskUserQuestionsConfig", + "AsyncTrueForge", + "AvailableMcpServer", + "AvailableModel", + "AvailableModelProvider", + "AvailableSkill", + "BadGatewayError", + "BadRequestError", + "BaseMcpAuthRequiredEvent", + "BaseThreadDoneEvent", + "CancelSessionResponse", + "CapabilitiesData", + "CatalogCustomModelProvider", + "CatalogMcpServer", + "CatalogMcpServerType", + "CatalogModel", + "CatalogModelProvider", + "CatalogSandboxProvider", + "CatalogSkill", + "CatalogSkillType", + "CatalogWellKnownModelProvider", + "CatalogWellKnownModelProviderType", + "ChatCompletionChunkDeltaToolCall", + "ChatCompletionChunkDeltaToolCallFunction", + "ChatCompletionContentPartRefusal", + "ChatCompletionContentPartText", + "ChatCompletionMessageToolCall", + "ChatCompletionMessageToolCallFunction", + "CompactionConfig", + "ConfiguredMcpServer", + "ConfiguredModel", + "ConfiguredModelProvider", + "ConfiguredSandboxProvider", + "ConfiguredSkill", + "ConflictError", + "ContentTooLargeError", + "ContextManagementConfig", + "CreateScheduleRunResponse", + "CreateSessionAgent", + "CreatedBySubject", + "CronExpression", + "CustomModelProvider", + "DaytonaSandboxProviderAuth", + "DefaultAioHttpClient", + "DefaultAsyncHttpxClient", + "DeleteAgentResponse", + "DeleteScheduleResponse", + "DynamicSubAgentsConfig", + "ExtendedChunkDeltaToolCall", + "FailedDependencyError", + "FileContent", + "FinishReason", + "FireworksModelProvider", + "ForbiddenError", + "GenerativeUiConfig", + "GetAgentCodeSnippetsResponse", + "GetAgentResponse", + "GetAvailableMcpServerResponse", + "GetCapabilitiesResponse", + "GetMcpServerCatalogResponse", + "GetMcpServerResponse", + "GetMeResponse", + "GetMeSubject", + "GetModelProviderCatalogResponse", + "GetModelProviderResponse", + "GetSandboxProviderCatalogResponse", + "GetSandboxProviderResponse", + "GetScheduleResponse", + "GetSessionMetricsChartDataResponse", + "GetSessionMetricsChartResponse", + "GetSessionMetricsMeterResponse", + "GetSessionResponse", + "GetSkillCatalogResponse", + "GetSkillResponse", + "GetTurnResponse", + "GitSkill", + "GoneError", + "GoogleGeminiModelProvider", + "InitialUserMessage", + "InputTokensCompactionTrigger", + "InternalServerError", + "LargeToolResponseConfig", + "ListAgentsResponse", + "ListAvailableMcpServersResponse", + "ListAvailableModelsResponse", + "ListAvailableSkillsResponse", + "ListMcpServerToolsResponse", + "ListMcpServersResponse", + "ListModelProvidersResponse", + "ListPermissionsResponse", + "ListScheduleRunsResponse", + "ListSchedulesResponse", + "ListSessionEventsResponse", + "ListSessionsOrder", + "ListSessionsResponse", + "ListSkillVersionsResponse", + "ListSkillsResponse", + "ListTurnEventsOrder", + "ListTurnEventsResponse", + "ListTurnsResponse", + "McpAuthRequiredEvent", + "McpAuthStatus", + "McpAuthStatusStatus", + "McpInitializeEvent", + "McpServer", + "McpServerApprovalToolSelector", + "McpServerAuthInfo", + "McpServerAuthPublic", + "McpServerAuthPublicOne", + "McpServerAuthPublicZero", + "McpServerDcrAuth", + "McpServerHeaderAuth", + "McpServerInitInfo", + "McpServerInitInfoTransportType", + "McpServerManifest", + "McpServerManifestAuth", + "McpServerToolSelector", + "McpToolInfo", + "Me", + "MeSessionType", + "MetricsUnit", + "Model", + "ModelMessageDeltaEvent", + "ModelMessageEvent", + "ModelMessageEventContent", + "ModelMessageEventContentOneItem", + "ModelMessageUsage", + "ModelMessageUsageInputTokensBreakdown", + "ModelParams", + "ModelProperties", + "ModelProviderAuth", + "ModelProviderManifest", + "MoonshotModelProvider", + "NotFoundError", + "OpenAiModelProvider", + "PermissionResourceType", + "PreconditionFailedError", + "PreviousTurnIdInput", + "RawToolCall", + "ReasoningEffort", + "RemoteMcpServerManifest", + "RequestErrorResponse", + "RequestErrorResponseError", + "ResourceName", + "ResourcePermission", + "ResponseFormat", + "ResponseFormatJsonObject", + "ResponseFormatJsonSchema", + "ResponseFormatJsonSchemaJsonSchema", + "ResponseFormatText", + "RuntimeConfig", + "SandboxBuildStatus", + "SandboxCapability", + "SandboxConfig", + "SandboxCreatedEvent", + "SandboxProviderManifest", + "Schedule", + "ScheduleManifest", + "ScheduleRun", + "ScheduleRunStatus", + "ScheduleStatus", + "Session", + "SessionAgent", + "SessionAgentInline", + "SessionAgentNameRef", + "SessionAgentReference", + "SessionAgentSpecBody", + "SessionEvent", + "SessionEventItem", + "SessionMetadata", + "SessionMetrics", + "SessionMetricsChart", + "SessionMetricsChartDataResponse", + "SessionMetricsChartName", + "SessionMetricsChartResponse", + "SessionMetricsGraph", + "SessionMetricsGraphLine", + "SessionMetricsMeter", + "SessionMetricsMeterName", + "SessionMetricsMeterResponse", + "SessionMetricsPoint", + "SessionSource", + "SessionSourceSchedule", + "SessionSourceType", + "SettingsCapability", + "Skill", + "SkillCapability", + "SkillManifest", + "SkillVersion", + "TextContent", + "ThreadCreatedEvent", + "ThreadDoneEvent", + "ThreadState", + "ThreadStateDone", + "ThreadStateError", + "Timezone", + "TogetherAiModelProvider", + "TokenPagination", + "ToolApprovalRequiredEvent", + "ToolCall", + "ToolCallRef", + "ToolInfo", + "ToolResponseEvent", + "ToolResponseRequiredEvent", + "TrueForge", + "TrueFoundryMcpServerManifest", + "TrueFoundryModelProvider", + "TrueFoundryRegistrySkill", + "TrueFoundrySystemToolInfo", + "Turn", + "TurnCreatedEvent", + "TurnDoneEvent", + "TurnDoneEventState", + "TurnInputItem", + "TurnMetrics", + "TurnState", + "TurnStateCancelled", + "TurnStateCancelledMetrics", + "TurnStateCancelledReason", + "TurnStateDone", + "TurnStateError", + "TurnStateErrorMetrics", + "TurnStateRunning", + "TurnStreamingEvent", + "UnauthorizedError", + "UnprocessableEntityError", + "UserMessage", + "UserMessageContent", + "UserMessageContentItem", + "UserToolApprovalEvent", + "UserToolResponseEvent", + "ZaiModelProvider", + "__version__", + "agents", + "auth", + "catalogs", + "internal", + "mcp_servers", + "models", + "schedules", + "server", + "sessions", + "settings", + "skills", +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/_default_clients.py b/python/trueforge_sdk/src/trueforge_sdk/_default_clients.py new file mode 100644 index 000000000..12f6533c9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/_default_clients.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +SDK_DEFAULT_TIMEOUT = 60 + +try: + import httpx_aiohttp # type: ignore[import-not-found] +except ImportError: + + class DefaultAioHttpClient(httpx.AsyncClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + raise RuntimeError( + "To use the aiohttp client, install the aiohttp extra: pip install trueforge_sdk[aiohttp]" + ) + +else: + + class DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +class DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) diff --git a/python/trueforge_sdk/src/trueforge_sdk/agents/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/agents/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/agents/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/agents/client.py b/python/trueforge_sdk/src/trueforge_sdk/agents/client.py new file mode 100644 index 000000000..26221e855 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/agents/client.py @@ -0,0 +1,507 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.agent_spec import AgentSpec +from ..types.delete_agent_response import DeleteAgentResponse +from ..types.get_agent_response import GetAgentResponse +from ..types.list_agents_response import ListAgentsResponse +from ..types.resource_name import ResourceName +from .raw_client import AsyncRawAgentsClient, RawAgentsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class AgentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAgentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAgentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAgentsClient + """ + return self._raw_client + + def list( + self, + *, + limit: typing.Optional[int] = 50, + page_token: typing.Optional[str] = None, + agent_name: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListAgentsResponse: + """ + List configured agents for the tenant, ordered by name. Optional `agent_name` filters by substring. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 50, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_name : typing.Optional[str] + Case-insensitive substring match on agent name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAgentsResponse + Paginated matching agents. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.agents.list() + """ + _response = self._raw_client.list( + limit=limit, page_token=page_token, agent_name=agent_name, request_options=request_options + ) + return _response.data + + def create( + self, + *, + description: str, + manifest: AgentSpec, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetAgentResponse: + """ + Creates an agent and allocates an immutable id. Fails if `name` is already taken. Name cannot be changed later. + + Parameters + ---------- + description : str + + manifest : AgentSpec + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentResponse + The created agent. + + Examples + -------- + from trueforge_sdk import AgentSpec, Model, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.agents.create( + description="description", + manifest=AgentSpec( + model=Model( + name="name", + ), + ), + name="name", + ) + """ + _response = self._raw_client.create( + description=description, manifest=manifest, name=name, request_options=request_options + ) + return _response.data + + def get(self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None) -> GetAgentResponse: + """ + Fetch a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentResponse + The agent. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.agents.get( + agent_id="agent_id", + ) + """ + _response = self._raw_client.get(agent_id=agent_id, request_options=request_options) + return _response.data + + def update( + self, + *, + agent_id: str, + manifest: AgentSpec, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetAgentResponse: + """ + Update an existing agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + manifest : AgentSpec + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentResponse + The saved agent. + + Examples + -------- + from trueforge_sdk import AgentSpec, Model, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.agents.update( + agent_id="agent_id", + manifest=AgentSpec( + model=Model( + name="name", + ), + ), + ) + """ + _response = self._raw_client.update( + agent_id=agent_id, manifest=manifest, description=description, request_options=request_options + ) + return _response.data + + def delete(self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None) -> DeleteAgentResponse: + """ + Delete a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteAgentResponse + Agent deleted. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.agents.delete( + agent_id="agent_id", + ) + """ + _response = self._raw_client.delete(agent_id=agent_id, request_options=request_options) + return _response.data + + +class AsyncAgentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAgentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAgentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAgentsClient + """ + return self._raw_client + + async def list( + self, + *, + limit: typing.Optional[int] = 50, + page_token: typing.Optional[str] = None, + agent_name: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListAgentsResponse: + """ + List configured agents for the tenant, ordered by name. Optional `agent_name` filters by substring. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 50, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_name : typing.Optional[str] + Case-insensitive substring match on agent name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAgentsResponse + Paginated matching agents. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.agents.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + limit=limit, page_token=page_token, agent_name=agent_name, request_options=request_options + ) + return _response.data + + async def create( + self, + *, + description: str, + manifest: AgentSpec, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetAgentResponse: + """ + Creates an agent and allocates an immutable id. Fails if `name` is already taken. Name cannot be changed later. + + Parameters + ---------- + description : str + + manifest : AgentSpec + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentResponse + The created agent. + + Examples + -------- + import asyncio + + from trueforge_sdk import AgentSpec, AsyncTrueForge, Model + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.agents.create( + description="description", + manifest=AgentSpec( + model=Model( + name="name", + ), + ), + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + description=description, manifest=manifest, name=name, request_options=request_options + ) + return _response.data + + async def get(self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None) -> GetAgentResponse: + """ + Fetch a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentResponse + The agent. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.agents.get( + agent_id="agent_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(agent_id=agent_id, request_options=request_options) + return _response.data + + async def update( + self, + *, + agent_id: str, + manifest: AgentSpec, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetAgentResponse: + """ + Update an existing agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + manifest : AgentSpec + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentResponse + The saved agent. + + Examples + -------- + import asyncio + + from trueforge_sdk import AgentSpec, AsyncTrueForge, Model + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.agents.update( + agent_id="agent_id", + manifest=AgentSpec( + model=Model( + name="name", + ), + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + agent_id=agent_id, manifest=manifest, description=description, request_options=request_options + ) + return _response.data + + async def delete( + self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteAgentResponse: + """ + Delete a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteAgentResponse + Agent deleted. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.agents.delete( + agent_id="agent_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(agent_id=agent_id, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/agents/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/agents/raw_client.py new file mode 100644 index 000000000..75d36a009 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/agents/raw_client.py @@ -0,0 +1,818 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.agent_spec import AgentSpec +from ..types.delete_agent_response import DeleteAgentResponse +from ..types.get_agent_response import GetAgentResponse +from ..types.list_agents_response import ListAgentsResponse +from ..types.request_error_response import RequestErrorResponse +from ..types.resource_name import ResourceName +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawAgentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + limit: typing.Optional[int] = 50, + page_token: typing.Optional[str] = None, + agent_name: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListAgentsResponse]: + """ + List configured agents for the tenant, ordered by name. Optional `agent_name` filters by substring. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 50, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_name : typing.Optional[str] + Case-insensitive substring match on agent name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListAgentsResponse] + Paginated matching agents. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/agents", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + "agent_name": agent_name, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAgentsResponse, + construct_type( + type_=ListAgentsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, + *, + description: str, + manifest: AgentSpec, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetAgentResponse]: + """ + Creates an agent and allocates an immutable id. Fails if `name` is already taken. Name cannot be changed later. + + Parameters + ---------- + description : str + + manifest : AgentSpec + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetAgentResponse] + The created agent. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/agents", + method="POST", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=AgentSpec, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentResponse, + construct_type( + type_=GetAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetAgentResponse]: + """ + Fetch a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetAgentResponse] + The agent. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentResponse, + construct_type( + type_=GetAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + *, + agent_id: str, + manifest: AgentSpec, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetAgentResponse]: + """ + Update an existing agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + manifest : AgentSpec + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetAgentResponse] + The saved agent. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}", + method="PUT", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=AgentSpec, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentResponse, + construct_type( + type_=GetAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete( + self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DeleteAgentResponse]: + """ + Delete a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteAgentResponse] + Agent deleted. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteAgentResponse, + construct_type( + type_=DeleteAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawAgentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + limit: typing.Optional[int] = 50, + page_token: typing.Optional[str] = None, + agent_name: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListAgentsResponse]: + """ + List configured agents for the tenant, ordered by name. Optional `agent_name` filters by substring. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 50, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_name : typing.Optional[str] + Case-insensitive substring match on agent name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListAgentsResponse] + Paginated matching agents. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/agents", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + "agent_name": agent_name, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAgentsResponse, + construct_type( + type_=ListAgentsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, + *, + description: str, + manifest: AgentSpec, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetAgentResponse]: + """ + Creates an agent and allocates an immutable id. Fails if `name` is already taken. Name cannot be changed later. + + Parameters + ---------- + description : str + + manifest : AgentSpec + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetAgentResponse] + The created agent. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/agents", + method="POST", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=AgentSpec, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentResponse, + construct_type( + type_=GetAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetAgentResponse]: + """ + Fetch a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetAgentResponse] + The agent. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentResponse, + construct_type( + type_=GetAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + *, + agent_id: str, + manifest: AgentSpec, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetAgentResponse]: + """ + Update an existing agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + manifest : AgentSpec + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetAgentResponse] + The saved agent. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}", + method="PUT", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=AgentSpec, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentResponse, + construct_type( + type_=GetAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, *, agent_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DeleteAgentResponse]: + """ + Delete a configured agent by immutable id. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteAgentResponse] + Agent deleted. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteAgentResponse, + construct_type( + type_=DeleteAgentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/auth/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/auth/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/auth/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/auth/client.py b/python/trueforge_sdk/src/trueforge_sdk/auth/client.py new file mode 100644 index 000000000..3870a7524 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/auth/client.py @@ -0,0 +1,102 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.get_me_response import GetMeResponse +from .raw_client import AsyncRawAuthClient, RawAuthClient + + +class AuthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAuthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAuthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAuthClient + """ + return self._raw_client + + def me(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetMeResponse: + """ + Returns the authenticated caller identity (`type`, `tenant_id`, `subject`, `roles`) wrapped as `{ data }`. `type` is `oidc-connected` when browser OIDC is enabled, otherwise `default`. When auth is enabled this requires a valid `id_token` cookie or `Authorization: Bearer` token (401 otherwise). When auth is disabled, returns the standalone default identity. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMeResponse + Caller identity for the current request. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.auth.me() + """ + _response = self._raw_client.me(request_options=request_options) + return _response.data + + +class AsyncAuthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAuthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAuthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAuthClient + """ + return self._raw_client + + async def me(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetMeResponse: + """ + Returns the authenticated caller identity (`type`, `tenant_id`, `subject`, `roles`) wrapped as `{ data }`. `type` is `oidc-connected` when browser OIDC is enabled, otherwise `default`. When auth is enabled this requires a valid `id_token` cookie or `Authorization: Bearer` token (401 otherwise). When auth is disabled, returns the standalone default identity. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMeResponse + Caller identity for the current request. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.auth.me() + + + asyncio.run(main()) + """ + _response = await self._raw_client.me(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/auth/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/auth/raw_client.py new file mode 100644 index 000000000..48b252b19 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/auth/raw_client.py @@ -0,0 +1,123 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.unauthorized_error import UnauthorizedError +from ..types.get_me_response import GetMeResponse +from ..types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawAuthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def me(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[GetMeResponse]: + """ + Returns the authenticated caller identity (`type`, `tenant_id`, `subject`, `roles`) wrapped as `{ data }`. `type` is `oidc-connected` when browser OIDC is enabled, otherwise `default`. When auth is enabled this requires a valid `id_token` cookie or `Authorization: Bearer` token (401 otherwise). When auth is disabled, returns the standalone default identity. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMeResponse] + Caller identity for the current request. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/auth/me", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMeResponse, + construct_type( + type_=GetMeResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawAuthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def me(self, *, request_options: typing.Optional[RequestOptions] = None) -> AsyncHttpResponse[GetMeResponse]: + """ + Returns the authenticated caller identity (`type`, `tenant_id`, `subject`, `roles`) wrapped as `{ data }`. `type` is `oidc-connected` when browser OIDC is enabled, otherwise `default`. When auth is enabled this requires a valid `id_token` cookie or `Authorization: Bearer` token (401 otherwise). When auth is disabled, returns the standalone default identity. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMeResponse] + Caller identity for the current request. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/auth/me", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMeResponse, + construct_type( + type_=GetMeResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/base_client.py b/python/trueforge_sdk/src/trueforge_sdk/base_client.py new file mode 100644 index 000000000..e086197b1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/base_client.py @@ -0,0 +1,395 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import httpx +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.logging import LogConfig, Logger + +if typing.TYPE_CHECKING: + from .agents.client import AgentsClient, AsyncAgentsClient + from .auth.client import AsyncAuthClient, AuthClient + from .catalogs.client import AsyncCatalogsClient, CatalogsClient + from .internal.client import AsyncInternalClient, InternalClient + from .mcp_servers.client import AsyncMcpServersClient, McpServersClient + from .models.client import AsyncModelsClient, ModelsClient + from .schedules.client import AsyncSchedulesClient, SchedulesClient + from .server.client import AsyncServerClient, ServerClient + from .sessions.client import AsyncSessionsClient, SessionsClient + from .settings.client import AsyncSettingsClient, SettingsClient + from .skills.client import AsyncSkillsClient, SkillsClient + + +class BaseTrueForge: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_max_retries = max_retries if max_retries is not None else 2 + self._client_wrapper = SyncClientWrapper( + base_url=base_url, + token=token, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._internal: typing.Optional[InternalClient] = None + self._agents: typing.Optional[AgentsClient] = None + self._auth: typing.Optional[AuthClient] = None + self._server: typing.Optional[ServerClient] = None + self._mcp_servers: typing.Optional[McpServersClient] = None + self._models: typing.Optional[ModelsClient] = None + self._schedules: typing.Optional[SchedulesClient] = None + self._sessions: typing.Optional[SessionsClient] = None + self._skills: typing.Optional[SkillsClient] = None + self._catalogs: typing.Optional[CatalogsClient] = None + self._settings: typing.Optional[SettingsClient] = None + + @property + def internal(self): + if self._internal is None: + from .internal.client import InternalClient # noqa: E402 + + self._internal = InternalClient(client_wrapper=self._client_wrapper) + return self._internal + + @property + def agents(self): + if self._agents is None: + from .agents.client import AgentsClient # noqa: E402 + + self._agents = AgentsClient(client_wrapper=self._client_wrapper) + return self._agents + + @property + def auth(self): + if self._auth is None: + from .auth.client import AuthClient # noqa: E402 + + self._auth = AuthClient(client_wrapper=self._client_wrapper) + return self._auth + + @property + def server(self): + if self._server is None: + from .server.client import ServerClient # noqa: E402 + + self._server = ServerClient(client_wrapper=self._client_wrapper) + return self._server + + @property + def mcp_servers(self): + if self._mcp_servers is None: + from .mcp_servers.client import McpServersClient # noqa: E402 + + self._mcp_servers = McpServersClient(client_wrapper=self._client_wrapper) + return self._mcp_servers + + @property + def models(self): + if self._models is None: + from .models.client import ModelsClient # noqa: E402 + + self._models = ModelsClient(client_wrapper=self._client_wrapper) + return self._models + + @property + def schedules(self): + if self._schedules is None: + from .schedules.client import SchedulesClient # noqa: E402 + + self._schedules = SchedulesClient(client_wrapper=self._client_wrapper) + return self._schedules + + @property + def sessions(self): + if self._sessions is None: + from .sessions.client import SessionsClient # noqa: E402 + + self._sessions = SessionsClient(client_wrapper=self._client_wrapper) + return self._sessions + + @property + def skills(self): + if self._skills is None: + from .skills.client import SkillsClient # noqa: E402 + + self._skills = SkillsClient(client_wrapper=self._client_wrapper) + return self._skills + + @property + def catalogs(self): + if self._catalogs is None: + from .catalogs.client import CatalogsClient # noqa: E402 + + self._catalogs = CatalogsClient(client_wrapper=self._client_wrapper) + return self._catalogs + + @property + def settings(self): + if self._settings is None: + from .settings.client import SettingsClient # noqa: E402 + + self._settings = SettingsClient(client_wrapper=self._client_wrapper) + return self._settings + + +def _make_default_async_client( + timeout: typing.Optional[float], + follow_redirects: typing.Optional[bool], +) -> httpx.AsyncClient: + try: + import httpx_aiohttp # type: ignore[import-not-found] + except ImportError: + pass + else: + if follow_redirects is not None: + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout) + + if follow_redirects is not None: + return httpx.AsyncClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx.AsyncClient(timeout=timeout) + + +class AsyncBaseTrueForge: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + async_token : typing.Optional[typing.Callable[[], typing.Awaitable[str]]] + An async callable that returns a bearer token. Use this when token acquisition involves async I/O (e.g., refreshing tokens via an async HTTP client). When provided, this is used instead of the synchronous token for async requests. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_max_retries = max_retries if max_retries is not None else 2 + self._client_wrapper = AsyncClientWrapper( + base_url=base_url, + token=token, + headers=headers, + async_token=async_token, + httpx_client=httpx_client + if httpx_client is not None + else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), + timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._internal: typing.Optional[AsyncInternalClient] = None + self._agents: typing.Optional[AsyncAgentsClient] = None + self._auth: typing.Optional[AsyncAuthClient] = None + self._server: typing.Optional[AsyncServerClient] = None + self._mcp_servers: typing.Optional[AsyncMcpServersClient] = None + self._models: typing.Optional[AsyncModelsClient] = None + self._schedules: typing.Optional[AsyncSchedulesClient] = None + self._sessions: typing.Optional[AsyncSessionsClient] = None + self._skills: typing.Optional[AsyncSkillsClient] = None + self._catalogs: typing.Optional[AsyncCatalogsClient] = None + self._settings: typing.Optional[AsyncSettingsClient] = None + + @property + def internal(self): + if self._internal is None: + from .internal.client import AsyncInternalClient # noqa: E402 + + self._internal = AsyncInternalClient(client_wrapper=self._client_wrapper) + return self._internal + + @property + def agents(self): + if self._agents is None: + from .agents.client import AsyncAgentsClient # noqa: E402 + + self._agents = AsyncAgentsClient(client_wrapper=self._client_wrapper) + return self._agents + + @property + def auth(self): + if self._auth is None: + from .auth.client import AsyncAuthClient # noqa: E402 + + self._auth = AsyncAuthClient(client_wrapper=self._client_wrapper) + return self._auth + + @property + def server(self): + if self._server is None: + from .server.client import AsyncServerClient # noqa: E402 + + self._server = AsyncServerClient(client_wrapper=self._client_wrapper) + return self._server + + @property + def mcp_servers(self): + if self._mcp_servers is None: + from .mcp_servers.client import AsyncMcpServersClient # noqa: E402 + + self._mcp_servers = AsyncMcpServersClient(client_wrapper=self._client_wrapper) + return self._mcp_servers + + @property + def models(self): + if self._models is None: + from .models.client import AsyncModelsClient # noqa: E402 + + self._models = AsyncModelsClient(client_wrapper=self._client_wrapper) + return self._models + + @property + def schedules(self): + if self._schedules is None: + from .schedules.client import AsyncSchedulesClient # noqa: E402 + + self._schedules = AsyncSchedulesClient(client_wrapper=self._client_wrapper) + return self._schedules + + @property + def sessions(self): + if self._sessions is None: + from .sessions.client import AsyncSessionsClient # noqa: E402 + + self._sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + return self._sessions + + @property + def skills(self): + if self._skills is None: + from .skills.client import AsyncSkillsClient # noqa: E402 + + self._skills = AsyncSkillsClient(client_wrapper=self._client_wrapper) + return self._skills + + @property + def catalogs(self): + if self._catalogs is None: + from .catalogs.client import AsyncCatalogsClient # noqa: E402 + + self._catalogs = AsyncCatalogsClient(client_wrapper=self._client_wrapper) + return self._catalogs + + @property + def settings(self): + if self._settings is None: + from .settings.client import AsyncSettingsClient # noqa: E402 + + self._settings = AsyncSettingsClient(client_wrapper=self._client_wrapper) + return self._settings diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py new file mode 100644 index 000000000..c8555edfe --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import mcp_servers, model_providers, sandbox_providers, skills +_dynamic_imports: typing.Dict[str, str] = { + "mcp_servers": ".mcp_servers", + "model_providers": ".model_providers", + "sandbox_providers": ".sandbox_providers", + "skills": ".skills", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["mcp_servers", "model_providers", "sandbox_providers", "skills"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py new file mode 100644 index 000000000..d74c24af1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py @@ -0,0 +1,120 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .raw_client import AsyncRawCatalogsClient, RawCatalogsClient + +if typing.TYPE_CHECKING: + from .mcp_servers.client import AsyncMcpServersClient, McpServersClient + from .model_providers.client import AsyncModelProvidersClient, ModelProvidersClient + from .sandbox_providers.client import AsyncSandboxProvidersClient, SandboxProvidersClient + from .skills.client import AsyncSkillsClient, SkillsClient + + +class CatalogsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawCatalogsClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._mcp_servers: typing.Optional[McpServersClient] = None + self._model_providers: typing.Optional[ModelProvidersClient] = None + self._sandbox_providers: typing.Optional[SandboxProvidersClient] = None + self._skills: typing.Optional[SkillsClient] = None + + @property + def with_raw_response(self) -> RawCatalogsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawCatalogsClient + """ + return self._raw_client + + @property + def mcp_servers(self): + if self._mcp_servers is None: + from .mcp_servers.client import McpServersClient # noqa: E402 + + self._mcp_servers = McpServersClient(client_wrapper=self._client_wrapper) + return self._mcp_servers + + @property + def model_providers(self): + if self._model_providers is None: + from .model_providers.client import ModelProvidersClient # noqa: E402 + + self._model_providers = ModelProvidersClient(client_wrapper=self._client_wrapper) + return self._model_providers + + @property + def sandbox_providers(self): + if self._sandbox_providers is None: + from .sandbox_providers.client import SandboxProvidersClient # noqa: E402 + + self._sandbox_providers = SandboxProvidersClient(client_wrapper=self._client_wrapper) + return self._sandbox_providers + + @property + def skills(self): + if self._skills is None: + from .skills.client import SkillsClient # noqa: E402 + + self._skills = SkillsClient(client_wrapper=self._client_wrapper) + return self._skills + + +class AsyncCatalogsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawCatalogsClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._mcp_servers: typing.Optional[AsyncMcpServersClient] = None + self._model_providers: typing.Optional[AsyncModelProvidersClient] = None + self._sandbox_providers: typing.Optional[AsyncSandboxProvidersClient] = None + self._skills: typing.Optional[AsyncSkillsClient] = None + + @property + def with_raw_response(self) -> AsyncRawCatalogsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawCatalogsClient + """ + return self._raw_client + + @property + def mcp_servers(self): + if self._mcp_servers is None: + from .mcp_servers.client import AsyncMcpServersClient # noqa: E402 + + self._mcp_servers = AsyncMcpServersClient(client_wrapper=self._client_wrapper) + return self._mcp_servers + + @property + def model_providers(self): + if self._model_providers is None: + from .model_providers.client import AsyncModelProvidersClient # noqa: E402 + + self._model_providers = AsyncModelProvidersClient(client_wrapper=self._client_wrapper) + return self._model_providers + + @property + def sandbox_providers(self): + if self._sandbox_providers is None: + from .sandbox_providers.client import AsyncSandboxProvidersClient # noqa: E402 + + self._sandbox_providers = AsyncSandboxProvidersClient(client_wrapper=self._client_wrapper) + return self._sandbox_providers + + @property + def skills(self): + if self._skills is None: + from .skills.client import AsyncSkillsClient # noqa: E402 + + self._skills = AsyncSkillsClient(client_wrapper=self._client_wrapper) + return self._skills diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/client.py new file mode 100644 index 000000000..acbafc222 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/client.py @@ -0,0 +1,102 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_mcp_server_catalog_response import GetMcpServerCatalogResponse +from .raw_client import AsyncRawMcpServersClient, RawMcpServersClient + + +class McpServersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMcpServersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMcpServersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMcpServersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetMcpServerCatalogResponse: + """ + Shipped MCP server presets (discovery-only). Copy into PUT /settings/mcp-servers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerCatalogResponse + Shipped MCP server presets. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.catalogs.mcp_servers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncMcpServersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMcpServersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMcpServersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMcpServersClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetMcpServerCatalogResponse: + """ + Shipped MCP server presets (discovery-only). Copy into PUT /settings/mcp-servers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerCatalogResponse + Shipped MCP server presets. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.catalogs.mcp_servers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/raw_client.py new file mode 100644 index 000000000..4a12f2ee9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/mcp_servers/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_mcp_server_catalog_response import GetMcpServerCatalogResponse +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawMcpServersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetMcpServerCatalogResponse]: + """ + Shipped MCP server presets (discovery-only). Copy into PUT /settings/mcp-servers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMcpServerCatalogResponse] + Shipped MCP server presets. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/catalogs/mcp-servers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerCatalogResponse, + construct_type( + type_=GetMcpServerCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawMcpServersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetMcpServerCatalogResponse]: + """ + Shipped MCP server presets (discovery-only). Copy into PUT /settings/mcp-servers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMcpServerCatalogResponse] + Shipped MCP server presets. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/catalogs/mcp-servers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerCatalogResponse, + construct_type( + type_=GetMcpServerCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/client.py new file mode 100644 index 000000000..73e93243f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/client.py @@ -0,0 +1,102 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_model_provider_catalog_response import GetModelProviderCatalogResponse +from .raw_client import AsyncRawModelProvidersClient, RawModelProvidersClient + + +class ModelProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawModelProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawModelProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawModelProvidersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetModelProviderCatalogResponse: + """ + Shipped model-provider presets (discovery-only). Copy into PUT /settings/model-providers to configure. Includes a `custom` sentinel with `supported_reasoning_efforts`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetModelProviderCatalogResponse + Shipped model-provider presets. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.catalogs.model_providers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncModelProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawModelProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawModelProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawModelProvidersClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetModelProviderCatalogResponse: + """ + Shipped model-provider presets (discovery-only). Copy into PUT /settings/model-providers to configure. Includes a `custom` sentinel with `supported_reasoning_efforts`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetModelProviderCatalogResponse + Shipped model-provider presets. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.catalogs.model_providers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/raw_client.py new file mode 100644 index 000000000..c619ffb34 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/model_providers/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_model_provider_catalog_response import GetModelProviderCatalogResponse +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawModelProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetModelProviderCatalogResponse]: + """ + Shipped model-provider presets (discovery-only). Copy into PUT /settings/model-providers to configure. Includes a `custom` sentinel with `supported_reasoning_efforts`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetModelProviderCatalogResponse] + Shipped model-provider presets. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/catalogs/model-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetModelProviderCatalogResponse, + construct_type( + type_=GetModelProviderCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawModelProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetModelProviderCatalogResponse]: + """ + Shipped model-provider presets (discovery-only). Copy into PUT /settings/model-providers to configure. Includes a `custom` sentinel with `supported_reasoning_efforts`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetModelProviderCatalogResponse] + Shipped model-provider presets. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/catalogs/model-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetModelProviderCatalogResponse, + construct_type( + type_=GetModelProviderCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/raw_client.py new file mode 100644 index 000000000..e98c39839 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/raw_client.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper + + +class RawCatalogsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + +class AsyncRawCatalogsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/client.py new file mode 100644 index 000000000..b6e70ee64 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/client.py @@ -0,0 +1,104 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_sandbox_provider_catalog_response import GetSandboxProviderCatalogResponse +from .raw_client import AsyncRawSandboxProvidersClient, RawSandboxProvidersClient + + +class SandboxProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSandboxProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSandboxProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSandboxProvidersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetSandboxProviderCatalogResponse: + """ + Shipped sandbox-provider presets (discovery-only). Copy into PUT /settings/sandbox-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxProviderCatalogResponse + Shipped sandbox-provider presets. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.catalogs.sandbox_providers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncSandboxProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSandboxProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSandboxProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSandboxProvidersClient + """ + return self._raw_client + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> GetSandboxProviderCatalogResponse: + """ + Shipped sandbox-provider presets (discovery-only). Copy into PUT /settings/sandbox-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxProviderCatalogResponse + Shipped sandbox-provider presets. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.catalogs.sandbox_providers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/raw_client.py new file mode 100644 index 000000000..2c23863bd --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/sandbox_providers/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_sandbox_provider_catalog_response import GetSandboxProviderCatalogResponse +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawSandboxProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSandboxProviderCatalogResponse]: + """ + Shipped sandbox-provider presets (discovery-only). Copy into PUT /settings/sandbox-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSandboxProviderCatalogResponse] + Shipped sandbox-provider presets. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/catalogs/sandbox-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxProviderCatalogResponse, + construct_type( + type_=GetSandboxProviderCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSandboxProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSandboxProviderCatalogResponse]: + """ + Shipped sandbox-provider presets (discovery-only). Copy into PUT /settings/sandbox-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSandboxProviderCatalogResponse] + Shipped sandbox-provider presets. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/catalogs/sandbox-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxProviderCatalogResponse, + construct_type( + type_=GetSandboxProviderCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/client.py new file mode 100644 index 000000000..00c053cef --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/client.py @@ -0,0 +1,102 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_skill_catalog_response import GetSkillCatalogResponse +from .raw_client import AsyncRawSkillsClient, RawSkillsClient + + +class SkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSkillsClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetSkillCatalogResponse: + """ + Shipped skill presets (discovery-only). Copy into PUT /settings/skills to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSkillCatalogResponse + Shipped skill presets. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.catalogs.skills.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSkillsClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetSkillCatalogResponse: + """ + Shipped skill presets (discovery-only). Copy into PUT /settings/skills to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSkillCatalogResponse + Shipped skill presets. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.catalogs.skills.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/raw_client.py new file mode 100644 index 000000000..bcbb8dbe7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/skills/raw_client.py @@ -0,0 +1,125 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_skill_catalog_response import GetSkillCatalogResponse +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawSkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[GetSkillCatalogResponse]: + """ + Shipped skill presets (discovery-only). Copy into PUT /settings/skills to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSkillCatalogResponse] + Shipped skill presets. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/catalogs/skills", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSkillCatalogResponse, + construct_type( + type_=GetSkillCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSkillCatalogResponse]: + """ + Shipped skill presets (discovery-only). Copy into PUT /settings/skills to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSkillCatalogResponse] + Shipped skill presets. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/catalogs/skills", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSkillCatalogResponse, + construct_type( + type_=GetSkillCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/client.py b/python/trueforge_sdk/src/trueforge_sdk/client.py new file mode 100644 index 000000000..648c9d448 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/client.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .base_client import AsyncBaseTrueForge, BaseTrueForge +from .core.logging import LogConfig, Logger + + +class TrueForge(BaseTrueForge): + def __init__( + self, + *, + base_url: str, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + super().__init__( + base_url=base_url, + token=token, + headers=headers, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + follow_redirects=follow_redirects, + httpx_client=httpx_client, + logging=logging, + ) + + +class AsyncTrueForge(AsyncBaseTrueForge): + def __init__( + self, + *, + base_url: str, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + super().__init__( + base_url=base_url, + token=token, + headers=headers, + async_token=async_token, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + follow_redirects=follow_redirects, + httpx_client=httpx_client, + logging=logging, + ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/core/__init__.py new file mode 100644 index 000000000..48239474b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/__init__.py @@ -0,0 +1,146 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_error import ApiError + from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper + from .datetime_utils import Rfc2822DateTime, parse_rfc2822_datetime, serialize_datetime + from .file import File, convert_file_dict_to_httpx_tuples, with_content_type + from .http_client import AsyncHttpClient, HttpClient + from .http_response import AsyncHttpResponse, HttpResponse + from .jsonable_encoder import encode_path_param, jsonable_encoder + from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger + from .pagination import AsyncPager, SyncPager + from .parse_error import ParsingError + from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, + ) + from .query_encoder import encode_query + from .remove_none_from_dict import remove_none_from_dict + from .request_options import RequestOptions + from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + from .stream import AsyncStream, Stream, StreamEvent + from .unchecked_base_model import UncheckedBaseModel, UnionMetadata, construct_type +_dynamic_imports: typing.Dict[str, str] = { + "ApiError": ".api_error", + "AsyncClientWrapper": ".client_wrapper", + "AsyncHttpClient": ".http_client", + "AsyncHttpResponse": ".http_response", + "AsyncPager": ".pagination", + "AsyncStream": ".stream", + "BaseClientWrapper": ".client_wrapper", + "ConsoleLogger": ".logging", + "FieldMetadata": ".serialization", + "File": ".file", + "HttpClient": ".http_client", + "HttpResponse": ".http_response", + "ILogger": ".logging", + "IS_PYDANTIC_V2": ".pydantic_utilities", + "LogConfig": ".logging", + "LogLevel": ".logging", + "Logger": ".logging", + "ParsingError": ".parse_error", + "RequestOptions": ".request_options", + "Rfc2822DateTime": ".datetime_utils", + "Stream": ".stream", + "StreamEvent": ".stream", + "SyncClientWrapper": ".client_wrapper", + "SyncPager": ".pagination", + "UncheckedBaseModel": ".unchecked_base_model", + "UnionMetadata": ".unchecked_base_model", + "UniversalBaseModel": ".pydantic_utilities", + "UniversalRootModel": ".pydantic_utilities", + "construct_type": ".unchecked_base_model", + "convert_and_respect_annotation_metadata": ".serialization", + "convert_file_dict_to_httpx_tuples": ".file", + "create_logger": ".logging", + "encode_path_param": ".jsonable_encoder", + "encode_query": ".query_encoder", + "jsonable_encoder": ".jsonable_encoder", + "parse_obj_as": ".pydantic_utilities", + "parse_rfc2822_datetime": ".datetime_utils", + "remove_none_from_dict": ".remove_none_from_dict", + "serialize_datetime": ".datetime_utils", + "universal_field_validator": ".pydantic_utilities", + "universal_root_validator": ".pydantic_utilities", + "update_forward_refs": ".pydantic_utilities", + "with_content_type": ".file", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "AsyncHttpResponse", + "AsyncPager", + "AsyncStream", + "BaseClientWrapper", + "ConsoleLogger", + "FieldMetadata", + "File", + "HttpClient", + "HttpResponse", + "ILogger", + "IS_PYDANTIC_V2", + "LogConfig", + "LogLevel", + "Logger", + "ParsingError", + "RequestOptions", + "Rfc2822DateTime", + "Stream", + "StreamEvent", + "SyncClientWrapper", + "SyncPager", + "UncheckedBaseModel", + "UnionMetadata", + "UniversalBaseModel", + "UniversalRootModel", + "construct_type", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "create_logger", + "encode_path_param", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "parse_rfc2822_datetime", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/api_error.py b/python/trueforge_sdk/src/trueforge_sdk/core/api_error.py new file mode 100644 index 000000000..6f850a60c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/client_wrapper.py b/python/trueforge_sdk/src/trueforge_sdk/core/client_wrapper.py new file mode 100644 index 000000000..3f9eb0df1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/client_wrapper.py @@ -0,0 +1,149 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient +from .logging import LogConfig, Logger + + +class BaseClientWrapper: + def __init__( + self, + *, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self._token = token + self._headers = headers + self._base_url = base_url + self._timeout = timeout + self._max_retries = max_retries + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._logging = logging + + def get_headers(self) -> typing.Dict[str, str]: + import platform + + headers: typing.Dict[str, str] = { + "User-Agent": "trueforge_sdk/0.1.0-rc.1", + "X-Fern-Language": "Python", + "X-Fern-Runtime": f"python/{platform.python_version()}", + "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", + "X-Fern-SDK-Name": "trueforge_sdk", + "X-Fern-SDK-Version": "0.1.0-rc.1", + **(self.get_custom_headers() or {}), + } + token = self._get_token() + if token is not None: + headers["Authorization"] = f"Bearer {token}" + return headers + + def _get_token(self) -> typing.Optional[str]: + if isinstance(self._token, str) or self._token is None: + return self._token + else: + return self._token() + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + def get_max_retries(self) -> int: + return self._max_retries + + def get_stream_reconnection_enabled(self) -> bool: + return self._stream_reconnection_enabled if self._stream_reconnection_enabled is not None else True + + def get_max_stream_reconnection_attempts(self) -> typing.Optional[int]: + return self._max_stream_reconnection_attempts + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + httpx_client: httpx.Client, + ): + super().__init__( + token=token, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + logging_config=self._logging, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__( + token=token, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._async_token = async_token + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + async_base_headers=self.async_get_headers, + logging_config=self._logging, + ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/datetime_utils.py b/python/trueforge_sdk/src/trueforge_sdk/core/datetime_utils.py new file mode 100644 index 000000000..a12b2ad03 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/datetime_utils.py @@ -0,0 +1,70 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +from email.utils import parsedate_to_datetime +from typing import Any + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + + +def parse_rfc2822_datetime(v: Any) -> dt.datetime: + """ + Parse an RFC 2822 datetime string (e.g., "Wed, 02 Oct 2002 13:00:00 GMT") + into a datetime object. If the value is already a datetime, return it as-is. + Falls back to ISO 8601 parsing if RFC 2822 parsing fails. + """ + if isinstance(v, dt.datetime): + return v + if isinstance(v, str): + try: + return parsedate_to_datetime(v) + except Exception: + pass + # Fallback to ISO 8601 parsing + return dt.datetime.fromisoformat(v.replace("Z", "+00:00")) + raise ValueError(f"Expected str or datetime, got {type(v)}") + + +class Rfc2822DateTime(dt.datetime): + """A datetime subclass that parses RFC 2822 date strings. + + On Pydantic V1, uses __get_validators__ for pre-validation. + On Pydantic V2, uses __get_pydantic_core_schema__ for BeforeValidator-style parsing. + """ + + @classmethod + def __get_validators__(cls): # type: ignore[no-untyped-def] + yield parse_rfc2822_datetime + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: # type: ignore[override] + from pydantic_core import core_schema + + return core_schema.no_info_before_validator_function(parse_rfc2822_datetime, core_schema.datetime_schema()) + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/enum.py b/python/trueforge_sdk/src/trueforge_sdk/core/enum.py new file mode 100644 index 000000000..a3d17a67b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/enum.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +Provides a StrEnum base class that works across Python versions. + +For Python >= 3.11, this re-exports the standard library enum.StrEnum. +For older Python versions, this defines a compatible StrEnum using the +(str, Enum) mixin pattern so that generated SDKs can use a single base +class in all supported Python versions. +""" + +import enum +import sys + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + + class StrEnum(str, enum.Enum): + pass diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/file.py b/python/trueforge_sdk/src/trueforge_sdk/core/file.py new file mode 100644 index 000000000..44b0d27c0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/force_multipart.py b/python/trueforge_sdk/src/trueforge_sdk/core/force_multipart.py new file mode 100644 index 000000000..5440913fd --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/force_multipart.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict + + +class ForceMultipartDict(Dict[str, Any]): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self) -> bool: + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_client.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_client.py new file mode 100644 index 000000000..3e5a78650 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_client.py @@ -0,0 +1,940 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import socket +import time +import typing +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .logging import LogConfig, Logger, create_logger +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter + + +def get_keepalive_socket_options( + idle: int = 60, + intvl: int = 30, + cnt: int = 5, +) -> typing.List[typing.Tuple[int, int, int]]: + """ + Build TCP keepalive socket options for the current platform. + + Keepalive probes keep otherwise-idle connections alive so that long, + non-streaming requests survive idle-connection reaping by a firewall, + load balancer, or NAT. The available socket constants are OS-dependent, + so each option is guarded and only emitted when the platform defines it: + + - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows). + - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern + Windows, but ``TCP_KEEPALIVE`` on macOS. + - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows. + + Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` / + ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every + connection the transport opens. + """ + opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] + idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None) + if idle_const: + opts.append((socket.IPPROTO_TCP, idle_const, idle)) + if hasattr(socket, "TCP_KEEPINTVL"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl)) + if hasattr(socket, "TCP_KEEPCNT"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt)) + return opts + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # 1. Check Retry-After header first + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) + + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) + + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _retry_timeout_from_retries(retries: int) -> float: + """Determine retry timeout using exponential backoff when no response is available.""" + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _should_retry(response: httpx.Response) -> bool: + return response.status_code >= 500 or response.status_code in [429, 408, 409] + + +_SENSITIVE_HEADERS = frozenset( + { + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", + } +) + + +def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]: + return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()} + + +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): + return remove_none_from_dict(data) + return data + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], + optional_body: bool = False, +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + # A whole body left at the sentinel was never passed by the caller, so it is absent + # rather than empty: the request carries no content and no `Content-Type`. + if omit is not None: + if json is omit: + json = None + if data is omit: + data = None + + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + has_additional_body_parameters = bool( + request_options is not None and request_options.get("additional_body_parameters") + ) + + # Only collapse empty dict to None when the body was not explicitly provided + # and there are no additional body parameters. This preserves explicit empty + # bodies (e.g., when an endpoint has a request body type but all fields are optional). + # `optional_body` marks an endpoint whose body the API does not require, where a body + # that ends up empty means the caller passed none of its properties, so the request is + # sent with no content and no `Content-Type`. + if json_body == {} and (json is None or optional_body) and not has_additional_body_parameters: + json_body = None + if data_body == {} and (data is None or optional_body) and not has_additional_body_parameters: + data_body = None + + return json_body, data_body + + +def drop_content_type_without_body( + headers: typing.Dict[str, typing.Any], + *, + json_body: typing.Optional[typing.Any], + data_body: typing.Optional[typing.Any], + optional_body: bool, +) -> typing.Dict[str, typing.Any]: + """Strip ``Content-Type`` from a request that carries no body. + + ``get_request_body`` drops the body of an ``optional_body`` endpoint when the caller + supplied none of it, but the endpoint still passes the content type it would have used. + A request that sends nothing must not advertise a media type, so a server that branches + on the header sees a bodyless call for what it is. + """ + if not optional_body or json_body is not None or data_body is not None: + return headers + return {key: value for key, value in headers.items() if key.lower() != "content-type"} + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + optional_body: bool = False, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + json_body, data_body = get_request_body( + json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + _request_headers = drop_content_type_without_body( + _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + time.sleep(_retry_timeout_from_retries(retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + optional_body: bool = False, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body( + json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body + ) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + _request_headers = drop_content_type_without_body( + _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.async_base_headers = async_base_headers + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + optional_body: bool = False, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body( + json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body + ) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + _request_headers = drop_content_type_without_body( + _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = await self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + optional_body: bool = False, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body( + json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body + ) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + _request_headers = drop_content_type_without_body( + _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + async with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_response.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_response.py new file mode 100644 index 000000000..9aa1e1888 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_response.py @@ -0,0 +1,63 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +# Generic to represent the underlying type of the data wrapped by the HTTP response. +T = TypeVar("T") + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers and status code.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + @property + def status_code(self) -> int: + return self._response.status_code + + @property + def response(self) -> httpx.Response: + return self._response + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/__init__.py new file mode 100644 index 000000000..730e5a338 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_api.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_api.py new file mode 100644 index 000000000..9ca5602c2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_api.py @@ -0,0 +1,455 @@ +# This file was auto-generated by Fern from our API Definition. + +import codecs +import re +import time +from contextlib import asynccontextmanager, contextmanager +from typing import ( + Any, + AsyncContextManager, + AsyncGenerator, + AsyncIterator, + Callable, + ContextManager, + Iterator, + Optional, +) + +import anyio +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + +MAX_LINE_SIZE: int = 1_048_576 # 1 MiB + +# Reconnection defaults, mirroring the TypeScript SDK's Stream implementation. +DEFAULT_MAX_RECONNECTION_ATTEMPTS: int = 5 +DEFAULT_RECONNECT_DELAY_MS: int = 1_000 +MAX_RECONNECT_DELAY_MS: int = 30_000 + + +# A reconnect callback re-issues the original request (with a ``Last-Event-ID`` +# header set to the supplied event id) and returns a *context manager* yielding +# a fresh streaming ``httpx.Response``. Sync clients supply a sync context +# manager; async clients supply an async one. +class EventSource: + def __init__( + self, + response: httpx.Response, + *, + resumable: bool = False, + stream_reconnection_enabled: bool = True, + max_stream_reconnection_attempts: Optional[int] = None, + stream_terminator: Optional[str] = None, + reconnect: Optional[Callable[[str], Any]] = None, + ) -> None: + self._response = response + self._resumable = resumable + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._stream_terminator = stream_terminator + self._reconnect = reconnect + + @staticmethod + def _is_event_stream(response: httpx.Response) -> bool: + content_type = response.headers.get("content-type", "").partition(";")[0] + return "text/event-stream" in content_type + + def _check_content_type(self) -> None: + if not self._is_event_stream(self._response): + content_type = self._response.headers.get("content-type", "").partition(";")[0] + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _is_reconnect_response_usable(self, response: httpx.Response) -> bool: + """Whether a reconnected response can be resumed as an SSE stream. + + ``httpx.stream`` does not raise on non-success status, so a resume that + returns an error page (e.g. ``200 text/html`` or a ``500`` body) would + otherwise be parsed as SSE and yield garbage/zero events. Such a + response is treated as a failed attempt (back off and retry) instead. + """ + return response.status_code < 400 and self._is_event_stream(response) + + def _get_charset(self, response: Optional[httpx.Response] = None) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + resolved = response if response is not None else self._response + content_type = resolved.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + @staticmethod + def _normalize_sse_line_endings(buf: str) -> str: + """Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n). + + A trailing \\r is preserved because it may pair with a leading \\n in + the next chunk to form a single \\r\\n terminator. + """ + buf = buf.replace("\r\n", "\n") + if buf.endswith("\r"): + return buf[:-1].replace("\r", "\n") + "\r" + return buf.replace("\r", "\n") + + def _new_text_decoder(self, response: Optional[httpx.Response] = None) -> "codecs.IncrementalDecoder": + return codecs.getincrementaldecoder(self._get_charset(response))(errors="replace") + + def _reconnect_applicable(self) -> bool: + """Whether reconnection is configured for this stream at all. + + This is the terminator-gating half of the reconnect decision, kept + separate from :meth:`_should_reconnect` (which additionally requires a + last *dispatched* id and an unexhausted attempt budget). The split lets + a mid-stream transport error terminate consistently: + - a stream that can never reconnect (non-resumable, no terminator, + disabled, or no callback) must re-raise the error to the caller, so a + truncated stream is not mistaken for a clean completion; + - a resumable stream that has merely run out of attempts (or has no id + to resume from) ends cleanly — the same way an exhausted empty/error + -body resume already does, matching the TypeScript ``return``. + """ + return ( + self._resumable + and self._stream_terminator is not None + and self._stream_reconnection_enabled + and self._reconnect is not None + ) + + def _should_reconnect(self, last_dispatched_id: Optional[str], reconnect_attempts: int) -> bool: + """Decide whether a prematurely-ended stream should be reconnected. + + Mirrors the TypeScript ``shouldReconnect`` gating: + - only resumable SSE endpoints with a configured terminator, reconnect + enabled, and a reconnect callback are eligible (see + :meth:`_reconnect_applicable`); + - a last *dispatched* event id must exist to resume from; + - the consecutive-failed-attempt cap must not be exceeded. + """ + if not self._reconnect_applicable(): + return False + if not last_dispatched_id: + return False + max_attempts = ( + self._max_stream_reconnection_attempts + if self._max_stream_reconnection_attempts is not None + else DEFAULT_MAX_RECONNECTION_ATTEMPTS + ) + if reconnect_attempts >= max_attempts: + return False + return True + + def _reconnect_delay_seconds(self, last_retry: Optional[int]) -> float: + """Backoff before a reconnect. + + Uses the server's most recent ``retry:`` directive (milliseconds) when + present, otherwise a default of ``DEFAULT_RECONNECT_DELAY_MS``, clamped + to ``MAX_RECONNECT_DELAY_MS``. + """ + base_ms = last_retry if (last_retry is not None and last_retry > 0) else DEFAULT_RECONNECT_DELAY_MS + return min(base_ms, MAX_RECONNECT_DELAY_MS) / 1000.0 + + def _sleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``time.sleep`` blocks the calling thread but remains interruptible by + # signals (e.g. ``KeyboardInterrupt``), which propagate out and abort + # the reconnect without issuing another request. + time.sleep(self._reconnect_delay_seconds(last_retry)) + + async def _asleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``anyio.sleep`` is cancellation-aware: if the consumer cancels the task + # or closes the async generator mid-delay, this raises (and no further + # request is issued) instead of blocking for the whole interval. + await anyio.sleep(self._reconnect_delay_seconds(last_retry)) + + def _decode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: + buf = "" + for chunk in response.iter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + yield from self._flush_decoder(buf, decoder, text_decoder) + + async def _adecode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> AsyncGenerator[ServerSentEvent, None]: + buf = "" + async for chunk in response.aiter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + for sse in self._flush_decoder(buf, decoder, text_decoder): + yield sse + + def _flush_decoder( + self, + buf: str, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + # Consecutive failed reconnection attempts. Reset to 0 whenever an event + # is successfully dispatched (reset-on-progress) — matching browser + # `EventSource` semantics: a server that emits >=1 event then drops on + # every connection can reconnect indefinitely. + reconnect_attempts = 0 + + # ``None`` means there is no live stream to read this iteration (e.g. a + # failed reconnect); the loop then re-evaluates the reconnect decision + # without re-reading an exhausted response. + response: Optional[httpx.Response] = self._response + # Context manager for a response we opened ourselves and must close. + # The initial response is owned by the caller, so it starts as None. + owned_cm: Optional[ContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._decode_response(response, decoder, text_decoder) + while True: + try: + sse = next(events) + except StopIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + # ``next`` is used rather than ``for`` so this cannot + # swallow a ``GeneratorExit`` raised at a ``yield``. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + self._sleep_before_reconnect(last_retry) + + # Close the previously-opened reconnect response before opening + # a new one so we never hold more than one extra connection. + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: ContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = cm.__enter__() + except Exception: + # A failed reconnect consumes an attempt; back off and retry. + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + # Null/empty body or a non-SSE/error response (e.g. 204/304, + # a 500, or an HTML error page): treat as a failed attempt. + response = None + continue + + response = new_response + # Drop any partial event left over from the dropped stream, but + # keep the last event id (per the SSE spec) and start a fresh + # incremental text decoder for the new connection. + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + reconnect_attempts = 0 + + response: Optional[httpx.Response] = self._response + owned_cm: Optional[AsyncContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._adecode_response(response, decoder, text_decoder) + while True: + try: + sse = await events.__anext__() + except StopAsyncIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + await self._asleep_before_reconnect(last_retry) + + if owned_cm is not None: + await owned_cm.__aexit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: AsyncContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = await cm.__aenter__() + except Exception: + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + response = None + continue + + response = new_response + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + # Shield the close so a cancellation delivered while reading a + # reconnected response still fully tears the connection down + # instead of leaking it until the client is closed. + with anyio.CancelScope(shield=True): + await owned_cm.__aexit__(None, None, None) + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_decoders.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_decoders.py new file mode 100644 index 000000000..1f6b35ec0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_decoders.py @@ -0,0 +1,74 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def reset_in_progress_event(self) -> None: + """Discard any partially-parsed (undispatched) event. + + Used when a stream ends mid-event before reconnecting: the buffered + ``event``/``data``/``retry`` fields of the never-dispatched event must + be dropped so they do not corrupt the first event of the reconnected + stream. Per the SSE spec the last event id is *not* reset here — it + persists across connections. + """ + self._event = "" + self._data = [] + self._retry = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_exceptions.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_exceptions.py new file mode 100644 index 000000000..81605a8a6 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_models.py b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_models.py new file mode 100644 index 000000000..1af57f8fd --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/jsonable_encoder.py b/python/trueforge_sdk/src/trueforge_sdk/core/jsonable_encoder.py new file mode 100644 index 000000000..f638cc9a4 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/jsonable_encoder.py @@ -0,0 +1,133 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". + # OMIT values should be excluded from serialized payloads. + if obj is Ellipsis: + return None + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + if value is Ellipsis: + continue + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + if item is Ellipsis: + continue + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) + + +def encode_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment. + + Ensures proper string conversion for all types, including + booleans which need lowercase 'true'/'false' rather than + Python's 'True'/'False'. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/logging.py b/python/trueforge_sdk/src/trueforge_sdk/core/logging.py new file mode 100644 index 000000000..e5e572458 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/logging.py @@ -0,0 +1,107 @@ +# This file was auto-generated by Fern from our API Definition. + +import logging +import typing + +LogLevel = typing.Literal["debug", "info", "warn", "error"] + +_LOG_LEVEL_MAP: typing.Dict[LogLevel, int] = { + "debug": 1, + "info": 2, + "warn": 3, + "error": 4, +} + + +class ILogger(typing.Protocol): + def debug(self, message: str, **kwargs: typing.Any) -> None: ... + def info(self, message: str, **kwargs: typing.Any) -> None: ... + def warn(self, message: str, **kwargs: typing.Any) -> None: ... + def error(self, message: str, **kwargs: typing.Any) -> None: ... + + +class ConsoleLogger: + _logger: logging.Logger + + def __init__(self) -> None: + self._logger = logging.getLogger("fern") + if not self._logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) + self._logger.addHandler(handler) + self._logger.setLevel(logging.DEBUG) + + def debug(self, message: str, **kwargs: typing.Any) -> None: + self._logger.debug(message, extra=kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + self._logger.info(message, extra=kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + self._logger.warning(message, extra=kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + self._logger.error(message, extra=kwargs) + + +class LogConfig(typing.TypedDict, total=False): + level: LogLevel + logger: ILogger + silent: bool + + +class Logger: + _level: int + _logger: ILogger + _silent: bool + + def __init__(self, *, level: LogLevel, logger: ILogger, silent: bool) -> None: + self._level = _LOG_LEVEL_MAP[level] + self._logger = logger + self._silent = silent + + def _should_log(self, level: LogLevel) -> bool: + return not self._silent and self._level <= _LOG_LEVEL_MAP[level] + + def is_debug(self) -> bool: + return self._should_log("debug") + + def is_info(self) -> bool: + return self._should_log("info") + + def is_warn(self) -> bool: + return self._should_log("warn") + + def is_error(self) -> bool: + return self._should_log("error") + + def debug(self, message: str, **kwargs: typing.Any) -> None: + if self.is_debug(): + self._logger.debug(message, **kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + if self.is_info(): + self._logger.info(message, **kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + if self.is_warn(): + self._logger.warn(message, **kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + if self.is_error(): + self._logger.error(message, **kwargs) + + +_default_logger: Logger = Logger(level="info", logger=ConsoleLogger(), silent=True) + + +def create_logger(config: typing.Optional[typing.Union[LogConfig, Logger]] = None) -> Logger: + if config is None: + return _default_logger + if isinstance(config, Logger): + return config + return Logger( + level=config.get("level", "info"), + logger=config.get("logger", ConsoleLogger()), + silent=config.get("silent", True), + ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/pagination.py b/python/trueforge_sdk/src/trueforge_sdk/core/pagination.py new file mode 100644 index 000000000..760b08995 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/pagination.py @@ -0,0 +1,82 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import AsyncIterator, Awaitable, Callable, Generic, Iterator, List, Optional, TypeVar + +# Generic to represent the underlying type of the results within a page +T = TypeVar("T") +# Generic to represent the type of the API response +R = TypeVar("R") + + +# SDKs implement a Page ABC per-pagination request, the endpoint then returns a pager that wraps this type +# for example, an endpoint will return SyncPager[UserPage] where UserPage implements the Page ABC. ex: +# +# SyncPager( +# has_next=response.list_metadata.after is not None, +# items=response.data, +# # This should be the outer function that returns the SyncPager again +# get_next=lambda: list(..., cursor: response.cursor) (or list(..., offset: offset + 1)) +# ) + + +@dataclass(frozen=True) +class SyncPager(Generic[T, R]): + get_next: Optional[Callable[[], Optional[SyncPager[T, R]]]] + has_next: bool + items: Optional[List[T]] + response: R + + # Here we type ignore the iterator to avoid a mypy error + # caused by the type conflict with Pydanitc's __iter__ method + # brought in by extending the base model + def __iter__(self) -> Iterator[T]: # type: ignore[override] + for page in self.iter_pages(): + if page.items is not None: + yield from page.items + + def iter_pages(self) -> Iterator[SyncPager[T, R]]: + page: Optional[SyncPager[T, R]] = self + while page is not None: + yield page + + if not page.has_next or page.get_next is None: + return + + page = page.get_next() + if page is None or page.items is None or len(page.items) == 0: + return + + def next_page(self) -> Optional[SyncPager[T, R]]: + return self.get_next() if self.get_next is not None else None + + +@dataclass(frozen=True) +class AsyncPager(Generic[T, R]): + get_next: Optional[Callable[[], Awaitable[Optional[AsyncPager[T, R]]]]] + has_next: bool + items: Optional[List[T]] + response: R + + async def __aiter__(self) -> AsyncIterator[T]: + async for page in self.iter_pages(): + if page.items is not None: + for item in page.items: + yield item + + async def iter_pages(self) -> AsyncIterator[AsyncPager[T, R]]: + page: Optional[AsyncPager[T, R]] = self + while page is not None: + yield page + + if not page.has_next or page.get_next is None: + return + + page = await page.get_next() + if page is None or page.items is None or len(page.items) == 0: + return + + async def next_page(self) -> Optional[AsyncPager[T, R]]: + return await self.get_next() if self.get_next is not None else None diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/parse_error.py b/python/trueforge_sdk/src/trueforge_sdk/core/parse_error.py new file mode 100644 index 000000000..4527c6a8a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/parse_error.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ParsingError(Exception): + """ + Raised when the SDK fails to parse/validate a response from the server. + This typically indicates that the server returned a response whose shape + does not match the expected schema. + """ + + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + cause: Optional[Exception] + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + cause: Optional[Exception] = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + self.cause = cause + super().__init__() + if cause is not None: + self.__cause__ = cause + + def __str__(self) -> str: + cause_str = f", cause: {self.cause}" if self.cause is not None else "" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}{cause_str}" diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/pydantic_utilities.py b/python/trueforge_sdk/src/trueforge_sdk/core/pydantic_utilities.py new file mode 100644 index 000000000..6587f5e18 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/pydantic_utilities.py @@ -0,0 +1,508 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import inspect +import json +import logging +from collections import defaultdict +from dataclasses import asdict +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +import pydantic +import typing_extensions +from pydantic.fields import FieldInfo as _FieldInfo + +_logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from .http_sse._models import ServerSentEvent + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + _datetime_adapter = pydantic.TypeAdapter(dt.datetime) # type: ignore[attr-defined] + _date_adapter = pydantic.TypeAdapter(dt.date) # type: ignore[attr-defined] + + def parse_datetime(value: Any) -> dt.datetime: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value + return _datetime_adapter.validate_python(value) + + def parse_date(value: Any) -> dt.date: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value.date() + if isinstance(value, dt.date): + return value + return _date_adapter.validate_python(value) + + # Avoid importing from pydantic.v1 to maintain Python 3.14 compatibility. + from typing import get_args as get_args # type: ignore[assignment] + from typing import get_origin as get_origin # type: ignore[assignment] + + def is_literal_type(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return typing_extensions.get_origin(tp) is typing_extensions.Literal + + def is_union(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return tp is Union or typing_extensions.get_origin(tp) is Union # type: ignore[comparison-overlap] + + # Inline encoders_by_type to avoid importing from pydantic.v1.json + import re as _re + from collections import deque as _deque + from decimal import Decimal as _Decimal + from enum import Enum as _Enum + from ipaddress import ( + IPv4Address as _IPv4Address, + ) + from ipaddress import ( + IPv4Interface as _IPv4Interface, + ) + from ipaddress import ( + IPv4Network as _IPv4Network, + ) + from ipaddress import ( + IPv6Address as _IPv6Address, + ) + from ipaddress import ( + IPv6Interface as _IPv6Interface, + ) + from ipaddress import ( + IPv6Network as _IPv6Network, + ) + from pathlib import Path as _Path + from types import GeneratorType as _GeneratorType + from uuid import UUID as _UUID + + from pydantic.fields import FieldInfo as ModelField # type: ignore[no-redef, assignment] + + def _decimal_encoder(dec_value: Any) -> Any: + if dec_value.as_tuple().exponent >= 0: + return int(dec_value) + return float(dec_value) + + encoders_by_type: Dict[Type[Any], Callable[[Any], Any]] = { # type: ignore[no-redef] + bytes: lambda o: o.decode(), + dt.date: lambda o: o.isoformat(), + dt.datetime: lambda o: o.isoformat(), + dt.time: lambda o: o.isoformat(), + dt.timedelta: lambda td: td.total_seconds(), + _Decimal: _decimal_encoder, + _Enum: lambda o: o.value, + frozenset: list, + _deque: list, + _GeneratorType: list, + _IPv4Address: str, + _IPv4Interface: str, + _IPv4Network: str, + _IPv6Address: str, + _IPv6Interface: str, + _IPv6Network: str, + _Path: str, + _re.Pattern: lambda o: o.pattern, + set: list, + _UUID: str, + } +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef, assignment] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: + """ + Parse a ServerSentEvent into the appropriate type. + + This function handles data-level discrimination where the discriminator + (e.g., 'type') is inside the 'data' payload. It parses the SSE data field + as JSON and deserializes it into the target type. + + Note: Protocol-level discrimination (where the discriminator comes from + the SSE event: field) is handled at code-generation time and does not + use this function. + + Args: + sse: The ServerSentEvent object to parse + type_: The target type to deserialize into + + Returns: + The parsed object of type T + + Note: + This function is only available in SDK contexts where http_sse module exists. + """ + sse_event = asdict(sse) + data_value = sse_event.get("data") + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) + + +_type_adapter_cache: Dict[int, Any] = {} + + +def _get_type_adapter(type_: Type[Any]) -> Any: + key = id(type_) + adapter = _type_adapter_cache.get(key) + if adapter is None: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + _type_adapter_cache[key] = adapter + return adapter + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + # convert_and_respect_annotation_metadata is required for TypedDict aliasing. + # + # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: + # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through + # unchanged so Pydantic can validate them. + # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic + # will not recognize those aliases during validation. + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + has_pydantic_aliases = False + if IS_PYDANTIC_V2: + for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined] + alias = getattr(field_info, "alias", None) + if alias is not None and alias != field_name: + has_pydantic_aliases = True + break + else: + for field in getattr(type_, "__fields__", {}).values(): + alias = getattr(field, "alias", None) + name = getattr(field, "name", None) + if alias is not None and name is not None and alias != name: + has_pydantic_aliases = True + break + + dealiased_object = ( + object_ + if has_pydantic_aliases + else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + ) + else: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = _get_type_adapter(type_) + return adapter.validate_python(dealiased_object) # type: ignore[no-any-return] + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_validator(mode="before") # type: ignore[attr-defined] + @classmethod + def _coerce_field_names_to_aliases(cls, data: Any) -> Any: + """ + Accept Python field names in input by rewriting them to their Pydantic aliases, + while avoiding silent collisions when a key could refer to multiple fields. + """ + if not isinstance(data, Mapping): + return data + + fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined] + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field_info in fields.items(): + alias = getattr(field_info, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + # Detect ambiguous keys: a key that is an alias for one field and a name for another. + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in data and name_to_alias[key] not in data: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(data.keys()) + rewritten: Dict[str, Any] = dict(data) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.dict() # type: ignore[attr-defined] + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @pydantic.root_validator(pre=True) + def _coerce_field_names_to_aliases(cls, values: Any) -> Any: + """ + Pydantic v1 equivalent of _coerce_field_names_to_aliases. + """ + if not isinstance(values, Mapping): + return values + + fields = getattr(cls, "__fields__", {}) + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field in fields.items(): + alias = getattr(field, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in values and name_to_alias[key] not in values: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(values.keys()) + rewritten: Dict[str, Any] = dict(values) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return cast( + Dict[str, Any], + convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"), + ) + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + # In Pydantic v2, for RootModel we always use "before" mode + # The custom validators transform the input value before the model is created + return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, _FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/query_encoder.py b/python/trueforge_sdk/src/trueforge_sdk/core/query_encoder.py new file mode 100644 index 000000000..3183001d4 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/remove_none_from_dict.py b/python/trueforge_sdk/src/trueforge_sdk/core/remove_none_from_dict.py new file mode 100644 index 000000000..c2298143f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/request_options.py b/python/trueforge_sdk/src/trueforge_sdk/core/request_options.py new file mode 100644 index 000000000..caa6f669b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/request_options.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout: float. The number of seconds to await an API call before timing out. + + - timeout_in_seconds: int. Deprecated alias for `timeout`; both are in seconds. Prefer `timeout`. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout: NotRequired[float] + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] + stream_reconnection_enabled: NotRequired[bool] + max_stream_reconnection_attempts: NotRequired[int] diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/serialization.py b/python/trueforge_sdk/src/trueforge_sdk/core/serialization.py new file mode 100644 index 000000000..1d753e26f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/serialization.py @@ -0,0 +1,347 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles +# forward-reference annotations. The result is constant for a given type, so we cache it. +# This is critical for hot paths like SSE event parsing, where the same (often large +# discriminated-union) type is converted on every single event. +_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {} + + +def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + cached = _type_hints_cache.get(expected_type) + except TypeError: + # Unhashable type; resolve without caching. + return _resolve_type_hints(expected_type) + if cached is None: + cached = _resolve_type_hints(expected_type) + _type_hints_cache[expected_type] = cached + return cached + + +def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + return typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The type contains a circular reference, so we use the __annotations__ attribute directly. + return getattr(expected_type, "__annotations__", {}) + + +# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given +# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias. +# This is constant per type, so we cache it and use it to short-circuit the recursive walk. +_requires_conversion_cache: typing.Dict[typing.Any, bool] = {} + + +def _requires_conversion(type_: typing.Any) -> bool: + try: + cached = _requires_conversion_cache.get(type_) + except TypeError: + # Unhashable annotation; compute without caching. + return _compute_requires_conversion(type_, set()) + if cached is None: + cached = _compute_requires_conversion(type_, set()) + _requires_conversion_cache[type_] = cached + return cached + + +def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool: + clean_type = _remove_annotations(type_) + + try: + if clean_type in seen: + return False + seen = seen | {clean_type} + except TypeError: + # Unhashable type; skip cycle tracking (the type graph is finite in practice). + pass + + # Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields. + if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict( + clean_type + ): + annotations = _get_cached_type_hints(clean_type) + if _get_alias_to_field_name(annotations): + return True + return any(_compute_requires_conversion(hint, seen) for hint in annotations.values()) + + # Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.). + return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type)) + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + # The only thing this function ever rewrites is keys that carry a FieldMetadata + # alias. If nothing in the (cached) type graph has such an alias, the conversion is + # a content-identity transform, so we can skip the entire recursive walk. This is + # the hot path for SSE streaming, where a large discriminated union would otherwise + # be traversed on every single event. + if not _requires_conversion(annotation): + return object_ + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = _get_cached_type_hints(expected_type) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = _get_cached_type_hints(type_) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = _get_cached_type_hints(type_) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/stream.py b/python/trueforge_sdk/src/trueforge_sdk/core/stream.py new file mode 100644 index 000000000..9d57e5ed0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/stream.py @@ -0,0 +1,148 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import types +import typing +from dataclasses import dataclass + +T = typing.TypeVar("T") + + +@dataclass(frozen=True) +class StreamEvent(typing.Generic[T]): + """A single event of a stream, exposing the server-sent event metadata alongside the parsed payload. + + `id`, `event` and `retry` are only populated for server-sent event streams; for JSON and text + streams, which have no event metadata, they are always `None`. + """ + + data: T + id: typing.Optional[str] = None + event: typing.Optional[str] = None + retry: typing.Optional[int] = None + + +class Stream(typing.Generic[T]): + """A stream of parsed events. + + Iterating the stream yields the parsed payloads, while `with_metadata()` yields them wrapped in a + `StreamEvent`, which also exposes the server-sent event `id`, `event` and `retry` fields: + + for chunk in client.service.stream(...): + ... + + for event in client.service.stream(...).with_metadata(): + print(event.id, event.data) + + The HTTP request is only issued once the stream is first iterated. The response is released when + the stream is exhausted, when iterating raises, when `close()` is called, when the stream is used + as a context manager, and when the stream is garbage collected (closing the stream closes the + underlying generator, which in turn closes the response). + """ + + def __init__(self, *, events: typing.Callable[[], typing.Generator[StreamEvent[T], None, None]]): + self._open_events = events + self._events: typing.Optional[typing.Generator[StreamEvent[T], None, None]] = None + self._closed = False + + def with_metadata(self) -> typing.Iterator[StreamEvent[T]]: + return self._events_generator() + + def close(self) -> None: + self._closed = True + events = self._events + self._events = None + if events is not None: + events.close() + + def __iter__(self) -> "Stream[T]": + return self + + def __next__(self) -> T: + return next(self._events_generator()).data + + def __enter__(self) -> "Stream[T]": + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_value: typing.Optional[BaseException], + traceback: typing.Optional[types.TracebackType], + ) -> None: + self.close() + + def _events_generator(self) -> typing.Generator[StreamEvent[T], None, None]: + if self._closed: + raise RuntimeError("The stream has already been closed.") + if self._events is None: + self._events = self._open_events() + return self._events + + +class AsyncStream(typing.Generic[T]): + """An asynchronous stream of parsed events. + + Iterating the stream yields the parsed payloads, while `with_metadata()` yields them wrapped in a + `StreamEvent`, which also exposes the server-sent event `id`, `event` and `retry` fields: + + async for chunk in client.service.stream(...): + ... + + async for event in (await client.service.stream(...)).with_metadata(): + print(event.id, event.data) + + The HTTP request is only issued once the stream is first iterated. The response is released when + the stream is exhausted, when iterating raises, when `close()` is called, when the stream is used + as an asynchronous context manager, and when the event loop finalizes the underlying asynchronous + generator. + """ + + def __init__(self, *, events: typing.Callable[[], typing.AsyncGenerator[StreamEvent[T], None]]): + self._open_events = events + self._events: typing.Optional[typing.AsyncGenerator[StreamEvent[T], None]] = None + self._closed = False + + def with_metadata(self) -> typing.AsyncIterator[StreamEvent[T]]: + return self._events_generator() + + async def close(self) -> None: + self._closed = True + events = self._events + self._events = None + if events is not None: + await events.aclose() + + async def aclose(self) -> None: + await self.close() + + def __aiter__(self) -> "AsyncStream[T]": + return self + + async def __anext__(self) -> T: + event = await self._events_generator().__anext__() + return event.data + + async def __aenter__(self) -> "AsyncStream[T]": + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_value: typing.Optional[BaseException], + traceback: typing.Optional[types.TracebackType], + ) -> None: + await self.close() + + def __await__(self) -> typing.Generator[None, None, "AsyncStream[T]"]: + """Returns the stream itself, so that it can be awaited before being iterated.""" + yield from () + return self + + def _events_generator(self) -> typing.AsyncGenerator[StreamEvent[T], None]: + if self._closed: + raise RuntimeError("The stream has already been closed.") + if self._events is None: + self._events = self._open_events() + return self._events diff --git a/python/trueforge_sdk/src/trueforge_sdk/core/unchecked_base_model.py b/python/trueforge_sdk/src/trueforge_sdk/core/unchecked_base_model.py new file mode 100644 index 000000000..c5deec1f5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/core/unchecked_base_model.py @@ -0,0 +1,494 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import enum +import inspect +import sys +import typing +import uuid + +import pydantic +import typing_extensions +from .pydantic_utilities import ( # type: ignore[attr-defined] + IS_PYDANTIC_V2, + ModelField, + UniversalBaseModel, + get_args, + get_origin, + is_literal_type, + is_union, + parse_date, + parse_datetime, + parse_obj_as, +) +from .serialization import get_field_to_alias_mapping +from pydantic_core import PydanticUndefined + + +class UnionMetadata: + discriminant: str + + def __init__(self, *, discriminant: str) -> None: + self.discriminant = discriminant + + +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +def _maybe_resolve_forward_ref( + type_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]], +) -> typing.Any: + """Resolve a ForwardRef using the module where *host* is defined. + + Pydantic v2 + ``from __future__ import annotations`` can leave field + annotations as ``list[ForwardRef('Block')]`` even after ``model_rebuild``. + Without resolution, ``construct_type`` sees a ForwardRef (not a class) and + skips recursive model construction, leaving nested data as raw dicts. + """ + if host is None or not isinstance(type_, typing.ForwardRef): + return type_ + mod = sys.modules.get(host.__module__) + if mod is None: + return type_ + try: + return eval(type_.__forward_arg__, vars(mod)) + except Exception: + return type_ + + +class UncheckedBaseModel(UniversalBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow + + @classmethod + def model_construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + # Fallback construct function to the specified override below. + return cls.construct(_fields_set=_fields_set, **values) + + # Allow construct to not validate model + # Implementation taken from: https://github.com/pydantic/pydantic/issues/1168#issuecomment-817742836 + @classmethod + def construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + m = cls.__new__(cls) + fields_values = {} + + if _fields_set is None: + _fields_set = set(values.keys()) + + fields = _get_model_fields(cls) + populate_by_name = _get_is_populate_by_name(cls) + field_aliases = get_field_to_alias_mapping(cls) + + for name, field in fields.items(): + # Key here is only used to pull data from the values dict + # you should always use the NAME of the field to for field_values, etc. + # because that's how the object is constructed from a pydantic perspective + key = field.alias + if (key is None or field.alias == name) and name in field_aliases: + key = field_aliases[name] + + if key is None or (key not in values and populate_by_name): # Added this to allow population by field name + key = name + + if key in values: + if IS_PYDANTIC_V2: + type_ = field.annotation # type: ignore # Pydantic v2 + else: + type_ = typing.cast(typing.Type, field.outer_type_) # type: ignore # Pydantic < v1.10.15 + + fields_values[name] = ( + construct_type(object_=values[key], type_=type_, host=cls) if type_ is not None else values[key] + ) + _fields_set.add(name) + else: + default = _get_field_default(field) + fields_values[name] = default + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None and default != PydanticUndefined: + _fields_set.add(name) + + # Add extras back in + extras = {} + pydantic_alias_fields = [field.alias for field in fields.values()] + internal_alias_fields = list(field_aliases.values()) + for key, value in values.items(): + # If the key is not a field by name, nor an alias to a field, then it's extra + if (key not in pydantic_alias_fields and key not in internal_alias_fields) and key not in fields: + if IS_PYDANTIC_V2: + extras[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if IS_PYDANTIC_V2: + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", extras) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + object.__setattr__(m, "__fields_set__", _fields_set) + m._init_private_attributes() # type: ignore # Pydantic v1 + return m + + +def _validate_collection_items_compatible(collection: typing.Any, target_type: typing.Type[typing.Any]) -> bool: + """ + Validate that all items in a collection are compatible with the target type. + + Args: + collection: The collection to validate (list, set, or dict values) + target_type: The target type to validate against + + Returns: + True if all items are compatible, False otherwise + """ + if inspect.isclass(target_type) and issubclass(target_type, pydantic.BaseModel): + for item in collection: + try: + # Try to validate the item against the target type + if isinstance(item, dict): + parse_obj_as(target_type, item) + else: + # If it's not a dict, it might already be the right type + if not isinstance(item, target_type): + return False + except Exception: + return False + return True + + +def _get_literal_field_value( + inner_type: typing.Type[typing.Any], field_name: str, field: typing.Any, object_: typing.Any +) -> typing.Any: + """Get the value of a Literal field from *object_*, checking both alias and field name.""" + name_or_alias = get_field_to_alias_mapping(inner_type).get(field_name, field_name) + pydantic_alias = getattr(field, "alias", None) + if isinstance(object_, dict): + if name_or_alias in object_: + return object_[name_or_alias] + if pydantic_alias and pydantic_alias != name_or_alias and pydantic_alias in object_: + return object_[pydantic_alias] + return None + return getattr(object_, name_or_alias, getattr(object_, pydantic_alias, None) if pydantic_alias else None) + + +def _literal_fields_match_strict(inner_type: typing.Type[typing.Any], object_: typing.Any) -> bool: + """Return True iff every Literal-typed field in *inner_type* is **present** in + *object_* and its value equals the field's declared default. + + This prevents models whose fields are all optional (e.g. ``FigureDetails``) + from vacuously matching inputs that don't carry the discriminant key at all + (e.g. ``{}`` for text blocks). For types with no Literal fields this + returns True unconditionally. + """ + fields = _get_model_fields(inner_type) + for field_name, field in fields.items(): + if IS_PYDANTIC_V2: + field_type = field.annotation # type: ignore # Pydantic v2 + else: + field_type = field.outer_type_ # type: ignore # Pydantic v1 + + if is_literal_type(field_type): # type: ignore[arg-type] + field_default = _get_field_default(field) + object_value = _get_literal_field_value(inner_type, field_name, field, object_) + if field_default != object_value: + return False + return True + + +def _convert_undiscriminated_union_type( + union_type: typing.Type[typing.Any], + object_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]] = None, +) -> typing.Any: + inner_types = get_args(union_type) + if typing.Any in inner_types: + return object_ + + # When any union member carries a Literal discriminant field, require the + # discriminant key to be present AND matching before accepting a candidate. + # This prevents models with all-optional fields (e.g. FigureDetails) from + # greedily matching inputs that belong to a different variant or to a + # plain-dict fallback (e.g. EmptyBlockDetails = Dict[str, Any]). + has_literal_discriminant = any( + inspect.isclass(t) + and issubclass(t, pydantic.BaseModel) + and any( + is_literal_type( + f.annotation if IS_PYDANTIC_V2 else f.outer_type_ # type: ignore + ) + for f in _get_model_fields(t).values() + ) + for t in inner_types + ) + + for inner_type in inner_types: + # Handle lists of objects that need parsing + if get_origin(inner_type) is list and isinstance(object_, list): + list_inner_type = _maybe_resolve_forward_ref(get_args(inner_type)[0], host) + try: + if inspect.isclass(list_inner_type) and issubclass(list_inner_type, pydantic.BaseModel): + # Validate that all items in the list are compatible with the target type + if _validate_collection_items_compatible(object_, list_inner_type): + parsed_list = [parse_obj_as(object_=item, type_=list_inner_type) for item in object_] + return parsed_list + except Exception: + pass + + try: + if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + if has_literal_discriminant and not _literal_fields_match_strict(inner_type, object_): + continue + # Attempt a validated parse until one works + return parse_obj_as(inner_type, object_) + except Exception: + continue + + # First pass: try types where all literal fields match the object's values. + for inner_type in inner_types: + if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + if has_literal_discriminant: + if not _literal_fields_match_strict(inner_type, object_): + continue + else: + # Legacy lenient check: skip only when a Literal value is + # present but doesn't match (allows absent-discriminant inputs). + fields = _get_model_fields(inner_type) + literal_fields_match = True + for field_name, field in fields.items(): + if IS_PYDANTIC_V2: + field_type = field.annotation # type: ignore # Pydantic v2 + else: + field_type = field.outer_type_ # type: ignore # Pydantic v1 + + if is_literal_type(field_type): # type: ignore[arg-type] + field_default = _get_field_default(field) + object_value = _get_literal_field_value(inner_type, field_name, field, object_) + if object_value is not None and field_default != object_value: + literal_fields_match = False + break + + if not literal_fields_match: + continue + + try: + return construct_type(object_=object_, type_=inner_type, host=host) + except Exception: + continue + + # Second pass: if no literal matches, return the first successful cast. + # When a Literal discriminant is present, skip Pydantic models whose + # discriminant doesn't match so that plain-dict fallback types are reached. + for inner_type in inner_types: + try: + if has_literal_discriminant and inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + if not _literal_fields_match_strict(inner_type, object_): + continue + return construct_type(object_=object_, type_=inner_type, host=host) + except Exception: + continue + + +def _convert_union_type( + type_: typing.Type[typing.Any], + object_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]] = None, +) -> typing.Any: + base_type = get_origin(type_) or type_ + union_type = type_ + if base_type == typing_extensions.Annotated: # type: ignore[comparison-overlap] + union_type = get_args(type_)[0] + annotated_metadata = get_args(type_)[1:] + for metadata in annotated_metadata: + if isinstance(metadata, UnionMetadata): + try: + # Cast to the correct type, based on the discriminant + for inner_type in get_args(union_type): + try: + objects_discriminant = getattr(object_, metadata.discriminant) + except: + objects_discriminant = object_[metadata.discriminant] + if inner_type.__fields__[metadata.discriminant].default == objects_discriminant: + return construct_type(object_=object_, type_=inner_type, host=host) + except Exception: + # Allow to fall through to our regular union handling + pass + return _convert_undiscriminated_union_type(union_type, object_, host) + + +def construct_type( + *, + type_: typing.Type[typing.Any], + object_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]] = None, +) -> typing.Any: + """ + Here we are essentially creating the same `construct` method in spirit as the above, but for all types, not just + Pydantic models. + The idea is to essentially attempt to coerce object_ to type_ (recursively) + """ + # Short circuit when dealing with optionals, don't try to coerces None to a type + if object_ is None: + return None + + base_type = get_origin(type_) or type_ + is_annotated = base_type == typing_extensions.Annotated # type: ignore[comparison-overlap] + maybe_annotation_members = get_args(type_) + is_annotated_union = is_annotated and is_union(get_origin(maybe_annotation_members[0])) + + if base_type == typing.Any: # type: ignore[comparison-overlap] + return object_ + + if base_type == dict: + if not isinstance(object_, typing.Mapping): + return object_ + + type_args = get_args(type_) + if not type_args: + return object_ + key_type, items_type = type_args + key_type = _maybe_resolve_forward_ref(key_type, host) + items_type = _maybe_resolve_forward_ref(items_type, host) + d = { + construct_type(object_=key, type_=key_type, host=host): construct_type( + object_=item, type_=items_type, host=host + ) + for key, item in object_.items() + } + return d + + if base_type == list: + if not isinstance(object_, list): + return object_ + + type_args = get_args(type_) + if not type_args: + return object_ + inner_type = _maybe_resolve_forward_ref(type_args[0], host) + return [construct_type(object_=entry, type_=inner_type, host=host) for entry in object_] + + if base_type == set: + if not isinstance(object_, set) and not isinstance(object_, list): + return object_ + + type_args = get_args(type_) + if not type_args: + return object_ + inner_type = _maybe_resolve_forward_ref(type_args[0], host) + return {construct_type(object_=entry, type_=inner_type, host=host) for entry in object_} + + if is_union(base_type) or is_annotated_union: + return _convert_union_type(type_, object_, host) + + # Cannot do an `issubclass` with a literal type, let's also just confirm we have a class before this call + if ( + object_ is not None + and not is_literal_type(type_) + and ( + (inspect.isclass(base_type) and issubclass(base_type, pydantic.BaseModel)) + or ( + is_annotated + and inspect.isclass(maybe_annotation_members[0]) + and issubclass(maybe_annotation_members[0], pydantic.BaseModel) + ) + ) + ): + if IS_PYDANTIC_V2: + return type_.model_construct(**object_) + else: + return type_.construct(**object_) + + if base_type == dt.datetime: + try: + return parse_datetime(object_) + except Exception: + return object_ + + if base_type == dt.date: + try: + return parse_date(object_) + except Exception: + return object_ + + if base_type == uuid.UUID: + try: + return uuid.UUID(object_) + except Exception: + return object_ + + if base_type == int: + try: + return int(object_) + except Exception: + return object_ + + if base_type == bool: + try: + if isinstance(object_, str): + stringified_object = object_.lower() + return stringified_object == "true" or stringified_object == "1" + + return bool(object_) + except Exception: + return object_ + + if inspect.isclass(base_type) and issubclass(base_type, enum.Enum): + try: + return base_type(object_) + except (ValueError, KeyError): + return object_ + + return object_ + + +def _get_is_populate_by_name(model: typing.Type["Model"]) -> bool: + if IS_PYDANTIC_V2: + return model.model_config.get("populate_by_name", False) # type: ignore # Pydantic v2 + return model.__config__.allow_population_by_field_name # type: ignore # Pydantic v1 + + +from pydantic.fields import FieldInfo as _FieldInfo + +PydanticField = typing.Union[ModelField, _FieldInfo] + + +# Pydantic V1 swapped the typing of __fields__'s values from ModelField to FieldInfo +# And so we try to handle both V1 cases, as well as V2 (FieldInfo from model.model_fields) +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/errors/__init__.py new file mode 100644 index 000000000..07f9aa6be --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/__init__.py @@ -0,0 +1,71 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_gateway_error import BadGatewayError + from .bad_request_error import BadRequestError + from .conflict_error import ConflictError + from .content_too_large_error import ContentTooLargeError + from .failed_dependency_error import FailedDependencyError + from .forbidden_error import ForbiddenError + from .gone_error import GoneError + from .internal_server_error import InternalServerError + from .not_found_error import NotFoundError + from .precondition_failed_error import PreconditionFailedError + from .unauthorized_error import UnauthorizedError + from .unprocessable_entity_error import UnprocessableEntityError +_dynamic_imports: typing.Dict[str, str] = { + "BadGatewayError": ".bad_gateway_error", + "BadRequestError": ".bad_request_error", + "ConflictError": ".conflict_error", + "ContentTooLargeError": ".content_too_large_error", + "FailedDependencyError": ".failed_dependency_error", + "ForbiddenError": ".forbidden_error", + "GoneError": ".gone_error", + "InternalServerError": ".internal_server_error", + "NotFoundError": ".not_found_error", + "PreconditionFailedError": ".precondition_failed_error", + "UnauthorizedError": ".unauthorized_error", + "UnprocessableEntityError": ".unprocessable_entity_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadGatewayError", + "BadRequestError", + "ConflictError", + "ContentTooLargeError", + "FailedDependencyError", + "ForbiddenError", + "GoneError", + "InternalServerError", + "NotFoundError", + "PreconditionFailedError", + "UnauthorizedError", + "UnprocessableEntityError", +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/bad_gateway_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/bad_gateway_error.py new file mode 100644 index 000000000..b72b76953 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/bad_gateway_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class BadGatewayError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=502, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/bad_request_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/bad_request_error.py new file mode 100644 index 000000000..11e1516bf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/bad_request_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class BadRequestError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/conflict_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/conflict_error.py new file mode 100644 index 000000000..5f845f61a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/conflict_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class ConflictError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=409, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/content_too_large_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/content_too_large_error.py new file mode 100644 index 000000000..3e5ae1c53 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/content_too_large_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class ContentTooLargeError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=413, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/failed_dependency_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/failed_dependency_error.py new file mode 100644 index 000000000..eeb40eb78 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/failed_dependency_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class FailedDependencyError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=424, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/forbidden_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/forbidden_error.py new file mode 100644 index 000000000..dce44d239 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/forbidden_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class ForbiddenError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=403, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/gone_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/gone_error.py new file mode 100644 index 000000000..454cd614a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/gone_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class GoneError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=410, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/internal_server_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/internal_server_error.py new file mode 100644 index 000000000..b71e2969f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/internal_server_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class InternalServerError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/not_found_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/not_found_error.py new file mode 100644 index 000000000..d945c7b1d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/not_found_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class NotFoundError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/precondition_failed_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/precondition_failed_error.py new file mode 100644 index 000000000..d0c170180 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/precondition_failed_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class PreconditionFailedError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=412, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/unauthorized_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/unauthorized_error.py new file mode 100644 index 000000000..e7af6a298 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/unauthorized_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class UnauthorizedError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/errors/unprocessable_entity_error.py b/python/trueforge_sdk/src/trueforge_sdk/errors/unprocessable_entity_error.py new file mode 100644 index 000000000..802dbabe2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/errors/unprocessable_entity_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.request_error_response import RequestErrorResponse + + +class UnprocessableEntityError(ApiError): + def __init__(self, body: RequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=422, headers=headers, body=body) diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/internal/__init__.py new file mode 100644 index 000000000..c58f0faa0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/__init__.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import agents, metrics, schedules, sessions +_dynamic_imports: typing.Dict[str, str] = { + "agents": ".agents", + "metrics": ".metrics", + "schedules": ".schedules", + "sessions": ".sessions", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["agents", "metrics", "schedules", "sessions"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/agents/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/internal/agents/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/agents/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/agents/client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/agents/client.py new file mode 100644 index 000000000..d9aed5f35 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/agents/client.py @@ -0,0 +1,134 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_agent_code_snippets_response import GetAgentCodeSnippetsResponse +from .raw_client import AsyncRawAgentsClient, RawAgentsClient + + +class AgentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAgentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAgentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAgentsClient + """ + return self._raw_client + + def get_code_snippets( + self, + *, + agent_id: str, + base_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetAgentCodeSnippetsResponse: + """ + TypeScript TrueForge SDK samples (stream and non-stream) for creating a session and turn against this agent. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + base_url : typing.Optional[str] + Public SDK base URL from the browser. When omitted, derived from the request origin and PUBLIC_BASE_URL. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentCodeSnippetsResponse + TypeScript SDK samples. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.agents.get_code_snippets( + agent_id="agent_id", + ) + """ + _response = self._raw_client.get_code_snippets( + agent_id=agent_id, base_url=base_url, request_options=request_options + ) + return _response.data + + +class AsyncAgentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAgentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAgentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAgentsClient + """ + return self._raw_client + + async def get_code_snippets( + self, + *, + agent_id: str, + base_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetAgentCodeSnippetsResponse: + """ + TypeScript TrueForge SDK samples (stream and non-stream) for creating a session and turn against this agent. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + base_url : typing.Optional[str] + Public SDK base URL from the browser. When omitted, derived from the request origin and PUBLIC_BASE_URL. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAgentCodeSnippetsResponse + TypeScript SDK samples. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.agents.get_code_snippets( + agent_id="agent_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_code_snippets( + agent_id=agent_id, base_url=base_url, request_options=request_options + ) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/agents/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/agents/raw_client.py new file mode 100644 index 000000000..9dbfe6ca7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/agents/raw_client.py @@ -0,0 +1,154 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import encode_path_param +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.not_found_error import NotFoundError +from ...types.get_agent_code_snippets_response import GetAgentCodeSnippetsResponse +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawAgentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_code_snippets( + self, + *, + agent_id: str, + base_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetAgentCodeSnippetsResponse]: + """ + TypeScript TrueForge SDK samples (stream and non-stream) for creating a session and turn against this agent. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + base_url : typing.Optional[str] + Public SDK base URL from the browser. When omitted, derived from the request origin and PUBLIC_BASE_URL. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetAgentCodeSnippetsResponse] + TypeScript SDK samples. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}/code-snippets", + method="GET", + params={ + "base_url": base_url, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentCodeSnippetsResponse, + construct_type( + type_=GetAgentCodeSnippetsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawAgentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_code_snippets( + self, + *, + agent_id: str, + base_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetAgentCodeSnippetsResponse]: + """ + TypeScript TrueForge SDK samples (stream and non-stream) for creating a session and turn against this agent. + + Parameters + ---------- + agent_id : str + Immutable agent identifier. + + base_url : typing.Optional[str] + Public SDK base URL from the browser. When omitted, derived from the request origin and PUBLIC_BASE_URL. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetAgentCodeSnippetsResponse] + TypeScript SDK samples. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/agents/{encode_path_param(agent_id)}/code-snippets", + method="GET", + params={ + "base_url": base_url, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAgentCodeSnippetsResponse, + construct_type( + type_=GetAgentCodeSnippetsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/client.py new file mode 100644 index 000000000..36f835c32 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/client.py @@ -0,0 +1,219 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.list_permissions_response import ListPermissionsResponse +from ..types.permission_resource_type import PermissionResourceType +from .raw_client import AsyncRawInternalClient, RawInternalClient + +if typing.TYPE_CHECKING: + from .agents.client import AgentsClient, AsyncAgentsClient + from .metrics.client import AsyncMetricsClient, MetricsClient + from .schedules.client import AsyncSchedulesClient, SchedulesClient + from .sessions.client import AsyncSessionsClient, SessionsClient +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class InternalClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawInternalClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._metrics: typing.Optional[MetricsClient] = None + self._schedules: typing.Optional[SchedulesClient] = None + self._sessions: typing.Optional[SessionsClient] = None + self._agents: typing.Optional[AgentsClient] = None + + @property + def with_raw_response(self) -> RawInternalClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawInternalClient + """ + return self._raw_client + + def list_permissions( + self, + *, + resource_ids: typing.Sequence[str], + resource_type: PermissionResourceType, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListPermissionsResponse: + """ + Return granted actions for each requested agent, schedule, or session id. + + Parameters + ---------- + resource_ids : typing.Sequence[str] + Resource ids of `resource_type` to evaluate for the caller. + + resource_type : PermissionResourceType + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListPermissionsResponse + Permissions keyed by resource id. + + Examples + -------- + from trueforge_sdk import PermissionResourceType, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.list_permissions( + resource_ids=["resource_ids"], + resource_type=PermissionResourceType.AGENT, + ) + """ + _response = self._raw_client.list_permissions( + resource_ids=resource_ids, resource_type=resource_type, request_options=request_options + ) + return _response.data + + @property + def metrics(self): + if self._metrics is None: + from .metrics.client import MetricsClient # noqa: E402 + + self._metrics = MetricsClient(client_wrapper=self._client_wrapper) + return self._metrics + + @property + def schedules(self): + if self._schedules is None: + from .schedules.client import SchedulesClient # noqa: E402 + + self._schedules = SchedulesClient(client_wrapper=self._client_wrapper) + return self._schedules + + @property + def sessions(self): + if self._sessions is None: + from .sessions.client import SessionsClient # noqa: E402 + + self._sessions = SessionsClient(client_wrapper=self._client_wrapper) + return self._sessions + + @property + def agents(self): + if self._agents is None: + from .agents.client import AgentsClient # noqa: E402 + + self._agents = AgentsClient(client_wrapper=self._client_wrapper) + return self._agents + + +class AsyncInternalClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawInternalClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._metrics: typing.Optional[AsyncMetricsClient] = None + self._schedules: typing.Optional[AsyncSchedulesClient] = None + self._sessions: typing.Optional[AsyncSessionsClient] = None + self._agents: typing.Optional[AsyncAgentsClient] = None + + @property + def with_raw_response(self) -> AsyncRawInternalClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawInternalClient + """ + return self._raw_client + + async def list_permissions( + self, + *, + resource_ids: typing.Sequence[str], + resource_type: PermissionResourceType, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListPermissionsResponse: + """ + Return granted actions for each requested agent, schedule, or session id. + + Parameters + ---------- + resource_ids : typing.Sequence[str] + Resource ids of `resource_type` to evaluate for the caller. + + resource_type : PermissionResourceType + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListPermissionsResponse + Permissions keyed by resource id. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, PermissionResourceType + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.list_permissions( + resource_ids=["resource_ids"], + resource_type=PermissionResourceType.AGENT, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_permissions( + resource_ids=resource_ids, resource_type=resource_type, request_options=request_options + ) + return _response.data + + @property + def metrics(self): + if self._metrics is None: + from .metrics.client import AsyncMetricsClient # noqa: E402 + + self._metrics = AsyncMetricsClient(client_wrapper=self._client_wrapper) + return self._metrics + + @property + def schedules(self): + if self._schedules is None: + from .schedules.client import AsyncSchedulesClient # noqa: E402 + + self._schedules = AsyncSchedulesClient(client_wrapper=self._client_wrapper) + return self._schedules + + @property + def sessions(self): + if self._sessions is None: + from .sessions.client import AsyncSessionsClient # noqa: E402 + + self._sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + return self._sessions + + @property + def agents(self): + if self._agents is None: + from .agents.client import AsyncAgentsClient # noqa: E402 + + self._agents = AsyncAgentsClient(client_wrapper=self._client_wrapper) + return self._agents diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/client.py new file mode 100644 index 000000000..d063531fe --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/client.py @@ -0,0 +1,366 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_session_metrics_chart_data_response import GetSessionMetricsChartDataResponse +from ...types.get_session_metrics_chart_response import GetSessionMetricsChartResponse +from ...types.get_session_metrics_meter_response import GetSessionMetricsMeterResponse +from ...types.session_metrics_chart_name import SessionMetricsChartName +from .raw_client import AsyncRawMetricsClient, RawMetricsClient + + +class MetricsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMetricsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMetricsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMetricsClient + """ + return self._raw_client + + def list_charts(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetSessionMetricsChartResponse: + """ + List available session metric charts. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionMetricsChartResponse + Available session metric charts. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.metrics.list_charts() + """ + _response = self._raw_client.list_charts(request_options=request_options) + return _response.data + + def get_chart_data( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + chart_name: SessionMetricsChartName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionMetricsChartDataResponse: + """ + Return one chart for the caller's sessions on a named agent over an inclusive creation-time window. Uses hourly buckets for windows up to 24 hours and daily UTC buckets otherwise. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + chart_name : SessionMetricsChartName + Session metrics chart to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionMetricsChartDataResponse + Zero-filled time series for one chart. + + Examples + -------- + import datetime + + from trueforge_sdk import SessionMetricsChartName, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.metrics.get_chart_data( + agent_id="agent_id", + start_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + end_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + chart_name=SessionMetricsChartName.SESSIONS_OVER_TIME, + ) + """ + _response = self._raw_client.get_chart_data( + agent_id=agent_id, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + chart_name=chart_name, + request_options=request_options, + ) + return _response.data + + def get_meters( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionMetricsMeterResponse: + """ + Aggregate the caller's session meters for a named agent over an inclusive creation-time window. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionMetricsMeterResponse + Session metric meters. + + Examples + -------- + import datetime + + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.metrics.get_meters( + agent_id="agent_id", + start_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + end_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + ) + """ + _response = self._raw_client.get_meters( + agent_id=agent_id, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + request_options=request_options, + ) + return _response.data + + +class AsyncMetricsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMetricsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMetricsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMetricsClient + """ + return self._raw_client + + async def list_charts( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> GetSessionMetricsChartResponse: + """ + List available session metric charts. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionMetricsChartResponse + Available session metric charts. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.metrics.list_charts() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_charts(request_options=request_options) + return _response.data + + async def get_chart_data( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + chart_name: SessionMetricsChartName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionMetricsChartDataResponse: + """ + Return one chart for the caller's sessions on a named agent over an inclusive creation-time window. Uses hourly buckets for windows up to 24 hours and daily UTC buckets otherwise. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + chart_name : SessionMetricsChartName + Session metrics chart to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionMetricsChartDataResponse + Zero-filled time series for one chart. + + Examples + -------- + import asyncio + import datetime + + from trueforge_sdk import AsyncTrueForge, SessionMetricsChartName + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.metrics.get_chart_data( + agent_id="agent_id", + start_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + end_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + chart_name=SessionMetricsChartName.SESSIONS_OVER_TIME, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_chart_data( + agent_id=agent_id, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + chart_name=chart_name, + request_options=request_options, + ) + return _response.data + + async def get_meters( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionMetricsMeterResponse: + """ + Aggregate the caller's session meters for a named agent over an inclusive creation-time window. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionMetricsMeterResponse + Session metric meters. + + Examples + -------- + import asyncio + import datetime + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.metrics.get_meters( + agent_id="agent_id", + start_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + end_timestamp=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_meters( + agent_id=agent_id, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + request_options=request_options, + ) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/raw_client.py new file mode 100644 index 000000000..2ab0ab383 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/metrics/raw_client.py @@ -0,0 +1,400 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.datetime_utils import serialize_datetime +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...types.get_session_metrics_chart_data_response import GetSessionMetricsChartDataResponse +from ...types.get_session_metrics_chart_response import GetSessionMetricsChartResponse +from ...types.get_session_metrics_meter_response import GetSessionMetricsMeterResponse +from ...types.request_error_response import RequestErrorResponse +from ...types.session_metrics_chart_name import SessionMetricsChartName +from pydantic import ValidationError + + +class RawMetricsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_charts( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSessionMetricsChartResponse]: + """ + List available session metric charts. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionMetricsChartResponse] + Available session metric charts. + """ + _response = self._client_wrapper.httpx_client.request( + "api/internal/metrics/charts", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionMetricsChartResponse, + construct_type( + type_=GetSessionMetricsChartResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_chart_data( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + chart_name: SessionMetricsChartName, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSessionMetricsChartDataResponse]: + """ + Return one chart for the caller's sessions on a named agent over an inclusive creation-time window. Uses hourly buckets for windows up to 24 hours and daily UTC buckets otherwise. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + chart_name : SessionMetricsChartName + Session metrics chart to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionMetricsChartDataResponse] + Zero-filled time series for one chart. + """ + _response = self._client_wrapper.httpx_client.request( + "api/internal/metrics/charts-data", + method="GET", + params={ + "agent_id": agent_id, + "start_timestamp": serialize_datetime(start_timestamp), + "end_timestamp": serialize_datetime(end_timestamp), + "chart_name": chart_name, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionMetricsChartDataResponse, + construct_type( + type_=GetSessionMetricsChartDataResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_meters( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSessionMetricsMeterResponse]: + """ + Aggregate the caller's session meters for a named agent over an inclusive creation-time window. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionMetricsMeterResponse] + Session metric meters. + """ + _response = self._client_wrapper.httpx_client.request( + "api/internal/metrics/meters", + method="GET", + params={ + "agent_id": agent_id, + "start_timestamp": serialize_datetime(start_timestamp), + "end_timestamp": serialize_datetime(end_timestamp), + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionMetricsMeterResponse, + construct_type( + type_=GetSessionMetricsMeterResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawMetricsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_charts( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSessionMetricsChartResponse]: + """ + List available session metric charts. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionMetricsChartResponse] + Available session metric charts. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/internal/metrics/charts", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionMetricsChartResponse, + construct_type( + type_=GetSessionMetricsChartResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_chart_data( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + chart_name: SessionMetricsChartName, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSessionMetricsChartDataResponse]: + """ + Return one chart for the caller's sessions on a named agent over an inclusive creation-time window. Uses hourly buckets for windows up to 24 hours and daily UTC buckets otherwise. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + chart_name : SessionMetricsChartName + Session metrics chart to return. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionMetricsChartDataResponse] + Zero-filled time series for one chart. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/internal/metrics/charts-data", + method="GET", + params={ + "agent_id": agent_id, + "start_timestamp": serialize_datetime(start_timestamp), + "end_timestamp": serialize_datetime(end_timestamp), + "chart_name": chart_name, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionMetricsChartDataResponse, + construct_type( + type_=GetSessionMetricsChartDataResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_meters( + self, + *, + agent_id: str, + start_timestamp: dt.datetime, + end_timestamp: dt.datetime, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSessionMetricsMeterResponse]: + """ + Aggregate the caller's session meters for a named agent over an inclusive creation-time window. + + Parameters + ---------- + agent_id : str + Named agent identifier. + + start_timestamp : dt.datetime + Inclusive lower bound on session `created_at`. + + end_timestamp : dt.datetime + Inclusive upper bound on session `created_at`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionMetricsMeterResponse] + Session metric meters. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/internal/metrics/meters", + method="GET", + params={ + "agent_id": agent_id, + "start_timestamp": serialize_datetime(start_timestamp), + "end_timestamp": serialize_datetime(end_timestamp), + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionMetricsMeterResponse, + construct_type( + type_=GetSessionMetricsMeterResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/raw_client.py new file mode 100644 index 000000000..7cb7947e6 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/raw_client.py @@ -0,0 +1,188 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.list_permissions_response import ListPermissionsResponse +from ..types.permission_resource_type import PermissionResourceType +from ..types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawInternalClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_permissions( + self, + *, + resource_ids: typing.Sequence[str], + resource_type: PermissionResourceType, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListPermissionsResponse]: + """ + Return granted actions for each requested agent, schedule, or session id. + + Parameters + ---------- + resource_ids : typing.Sequence[str] + Resource ids of `resource_type` to evaluate for the caller. + + resource_type : PermissionResourceType + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListPermissionsResponse] + Permissions keyed by resource id. + """ + _response = self._client_wrapper.httpx_client.request( + "api/internal/list-permissions", + method="POST", + json={ + "resource_ids": resource_ids, + "resource_type": resource_type, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListPermissionsResponse, + construct_type( + type_=ListPermissionsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawInternalClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_permissions( + self, + *, + resource_ids: typing.Sequence[str], + resource_type: PermissionResourceType, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListPermissionsResponse]: + """ + Return granted actions for each requested agent, schedule, or session id. + + Parameters + ---------- + resource_ids : typing.Sequence[str] + Resource ids of `resource_type` to evaluate for the caller. + + resource_type : PermissionResourceType + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListPermissionsResponse] + Permissions keyed by resource id. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/internal/list-permissions", + method="POST", + json={ + "resource_ids": resource_ids, + "resource_type": resource_type, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListPermissionsResponse, + construct_type( + type_=ListPermissionsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/client.py new file mode 100644 index 000000000..659200af3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/client.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from .raw_client import AsyncRawSchedulesClient, RawSchedulesClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SchedulesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSchedulesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSchedulesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSchedulesClient + """ + return self._raw_client + + def execute_run(self, *, schedule_run_id: str, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Execute a persisted schedule run using its saved schedule and agent. + + Parameters + ---------- + schedule_run_id : str + Immutable schedule run identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.schedules.execute_run( + schedule_run_id="schedule_run_id", + ) + """ + _response = self._raw_client.execute_run(schedule_run_id=schedule_run_id, request_options=request_options) + return _response.data + + +class AsyncSchedulesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSchedulesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSchedulesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSchedulesClient + """ + return self._raw_client + + async def execute_run( + self, *, schedule_run_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Execute a persisted schedule run using its saved schedule and agent. + + Parameters + ---------- + schedule_run_id : str + Immutable schedule run identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.schedules.execute_run( + schedule_run_id="schedule_run_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.execute_run(schedule_run_id=schedule_run_id, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/raw_client.py new file mode 100644 index 000000000..079cb3890 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/schedules/raw_client.py @@ -0,0 +1,179 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.not_found_error import NotFoundError +from ...errors.unauthorized_error import UnauthorizedError +from ...errors.unprocessable_entity_error import UnprocessableEntityError +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSchedulesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def execute_run( + self, *, schedule_run_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Execute a persisted schedule run using its saved schedule and agent. + + Parameters + ---------- + schedule_run_id : str + Immutable schedule run identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + "api/internal/schedules/runs/execute", + method="POST", + json={ + "schedule_run_id": schedule_run_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSchedulesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def execute_run( + self, *, schedule_run_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Execute a persisted schedule run using its saved schedule and agent. + + Parameters + ---------- + schedule_run_id : str + Immutable schedule run identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/internal/schedules/runs/execute", + method="POST", + json={ + "schedule_run_id": schedule_run_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/client.py new file mode 100644 index 000000000..3c155cf6b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/client.py @@ -0,0 +1,149 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.create_session_agent import CreateSessionAgent +from ...types.get_session_response import GetSessionResponse +from ...types.session_source_schedule import SessionSourceSchedule +from .raw_client import AsyncRawSessionsClient, RawSessionsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSessionsClient + """ + return self._raw_client + + def get_or_create_by_external_id( + self, + *, + agent: CreateSessionAgent, + external_id: str, + source: typing.Optional[SessionSourceSchedule] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionResponse: + """ + Idempotent get-or-create: returns the existing session for this `external_id`, or creates one + + Parameters + ---------- + agent : CreateSessionAgent + + external_id : str + Caller-supplied id unique within the tenant. + + source : typing.Optional[SessionSourceSchedule] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session already existed for this external id. + + Examples + -------- + from trueforge_sdk import SessionAgentNameRef, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.internal.sessions.get_or_create_by_external_id( + agent=SessionAgentNameRef( + name="name", + ), + external_id="external_id", + ) + """ + _response = self._raw_client.get_or_create_by_external_id( + agent=agent, external_id=external_id, source=source, request_options=request_options + ) + return _response.data + + +class AsyncSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSessionsClient + """ + return self._raw_client + + async def get_or_create_by_external_id( + self, + *, + agent: CreateSessionAgent, + external_id: str, + source: typing.Optional[SessionSourceSchedule] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionResponse: + """ + Idempotent get-or-create: returns the existing session for this `external_id`, or creates one + + Parameters + ---------- + agent : CreateSessionAgent + + external_id : str + Caller-supplied id unique within the tenant. + + source : typing.Optional[SessionSourceSchedule] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session already existed for this external id. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, SessionAgentNameRef + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.internal.sessions.get_or_create_by_external_id( + agent=SessionAgentNameRef( + name="name", + ), + external_id="external_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_or_create_by_external_id( + agent=agent, external_id=external_id, source=source, request_options=request_options + ) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/raw_client.py new file mode 100644 index 000000000..b2088667a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/internal/sessions/raw_client.py @@ -0,0 +1,252 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...errors.forbidden_error import ForbiddenError +from ...errors.not_found_error import NotFoundError +from ...errors.unprocessable_entity_error import UnprocessableEntityError +from ...types.create_session_agent import CreateSessionAgent +from ...types.get_session_response import GetSessionResponse +from ...types.request_error_response import RequestErrorResponse +from ...types.session_source_schedule import SessionSourceSchedule +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_or_create_by_external_id( + self, + *, + agent: CreateSessionAgent, + external_id: str, + source: typing.Optional[SessionSourceSchedule] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSessionResponse]: + """ + Idempotent get-or-create: returns the existing session for this `external_id`, or creates one + + Parameters + ---------- + agent : CreateSessionAgent + + external_id : str + Caller-supplied id unique within the tenant. + + source : typing.Optional[SessionSourceSchedule] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionResponse] + Session already existed for this external id. + """ + _response = self._client_wrapper.httpx_client.request( + "api/internal/sessions/get-or-create-by-external-id", + method="POST", + json={ + "agent": convert_and_respect_annotation_metadata( + object_=agent, annotation=CreateSessionAgent, direction="write" + ), + "external_id": external_id, + "source": convert_and_respect_annotation_metadata( + object_=source, annotation=SessionSourceSchedule, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_or_create_by_external_id( + self, + *, + agent: CreateSessionAgent, + external_id: str, + source: typing.Optional[SessionSourceSchedule] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSessionResponse]: + """ + Idempotent get-or-create: returns the existing session for this `external_id`, or creates one + + Parameters + ---------- + agent : CreateSessionAgent + + external_id : str + Caller-supplied id unique within the tenant. + + source : typing.Optional[SessionSourceSchedule] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionResponse] + Session already existed for this external id. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/internal/sessions/get-or-create-by-external-id", + method="POST", + json={ + "agent": convert_and_respect_annotation_metadata( + object_=agent, annotation=CreateSessionAgent, direction="write" + ), + "external_id": external_id, + "source": convert_and_respect_annotation_metadata( + object_=source, annotation=SessionSourceSchedule, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/client.py b/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/client.py new file mode 100644 index 000000000..4bb0e997b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/client.py @@ -0,0 +1,424 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.get_available_mcp_server_response import GetAvailableMcpServerResponse +from ..types.get_mcp_server_response import GetMcpServerResponse +from ..types.list_available_mcp_servers_response import ListAvailableMcpServersResponse +from ..types.list_mcp_server_tools_response import ListMcpServerToolsResponse +from ..types.mcp_auth_status import McpAuthStatus +from .raw_client import AsyncRawMcpServersClient, RawMcpServersClient + + +class McpServersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMcpServersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMcpServersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMcpServersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAvailableMcpServersResponse: + """ + Configured MCP servers as a slim name/url list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAvailableMcpServersResponse + All configured MCP servers (chat projection). + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.mcp_servers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def get( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetAvailableMcpServerResponse: + """ + A single MCP server as the slim chat projection, with live per-user auth_status. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAvailableMcpServerResponse + The MCP server (chat projection). + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.mcp_servers.get( + name="name", + ) + """ + _response = self._raw_client.get(name=name, request_options=request_options) + return _response.data + + def authorize( + self, + *, + name: str, + return_to: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> McpAuthStatus: + """ + Returns current auth status. When OAuth is required, includes an authorization URL. Optional return_to is the post-consent landing path. + + Parameters + ---------- + name : str + MCP server name. + + return_to : typing.Optional[str] + Same-origin path to land in the browser after consent. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + McpAuthStatus + Either already authenticated, or an authorization URL to redirect to. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.mcp_servers.authorize( + name="name", + ) + """ + _response = self._raw_client.authorize(name=name, return_to=return_to, request_options=request_options) + return _response.data + + def delete_authorization( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetMcpServerResponse: + """ + Disconnects OAuth for the MCP server when applicable and returns the updated server with auth_status. No-op when the server does not use stored OAuth tokens. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The MCP server after disconnect. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.mcp_servers.delete_authorization( + name="name", + ) + """ + _response = self._raw_client.delete_authorization(name=name, request_options=request_options) + return _response.data + + def list_tools( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> ListMcpServerToolsResponse: + """ + All tools exposed by the given MCP server (non-paginated), as returned by the MCP `tools/list` call. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListMcpServerToolsResponse + All tools of the MCP server. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.mcp_servers.list_tools( + name="name", + ) + """ + _response = self._raw_client.list_tools(name=name, request_options=request_options) + return _response.data + + +class AsyncMcpServersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMcpServersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMcpServersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMcpServersClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAvailableMcpServersResponse: + """ + Configured MCP servers as a slim name/url list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAvailableMcpServersResponse + All configured MCP servers (chat projection). + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.mcp_servers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def get( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetAvailableMcpServerResponse: + """ + A single MCP server as the slim chat projection, with live per-user auth_status. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetAvailableMcpServerResponse + The MCP server (chat projection). + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.mcp_servers.get( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(name=name, request_options=request_options) + return _response.data + + async def authorize( + self, + *, + name: str, + return_to: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> McpAuthStatus: + """ + Returns current auth status. When OAuth is required, includes an authorization URL. Optional return_to is the post-consent landing path. + + Parameters + ---------- + name : str + MCP server name. + + return_to : typing.Optional[str] + Same-origin path to land in the browser after consent. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + McpAuthStatus + Either already authenticated, or an authorization URL to redirect to. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.mcp_servers.authorize( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.authorize(name=name, return_to=return_to, request_options=request_options) + return _response.data + + async def delete_authorization( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetMcpServerResponse: + """ + Disconnects OAuth for the MCP server when applicable and returns the updated server with auth_status. No-op when the server does not use stored OAuth tokens. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The MCP server after disconnect. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.mcp_servers.delete_authorization( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_authorization(name=name, request_options=request_options) + return _response.data + + async def list_tools( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> ListMcpServerToolsResponse: + """ + All tools exposed by the given MCP server (non-paginated), as returned by the MCP `tools/list` call. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListMcpServerToolsResponse + All tools of the MCP server. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.mcp_servers.list_tools( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_tools(name=name, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/raw_client.py new file mode 100644 index 000000000..a4c0cb917 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/mcp_servers/raw_client.py @@ -0,0 +1,766 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_gateway_error import BadGatewayError +from ..errors.bad_request_error import BadRequestError +from ..errors.failed_dependency_error import FailedDependencyError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.get_available_mcp_server_response import GetAvailableMcpServerResponse +from ..types.get_mcp_server_response import GetMcpServerResponse +from ..types.list_available_mcp_servers_response import ListAvailableMcpServersResponse +from ..types.list_mcp_server_tools_response import ListMcpServerToolsResponse +from ..types.mcp_auth_status import McpAuthStatus +from ..types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawMcpServersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListAvailableMcpServersResponse]: + """ + Configured MCP servers as a slim name/url list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListAvailableMcpServersResponse] + All configured MCP servers (chat projection). + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/mcp-servers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAvailableMcpServersResponse, + construct_type( + type_=ListAvailableMcpServersResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetAvailableMcpServerResponse]: + """ + A single MCP server as the slim chat projection, with live per-user auth_status. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetAvailableMcpServerResponse] + The MCP server (chat projection). + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAvailableMcpServerResponse, + construct_type( + type_=GetAvailableMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def authorize( + self, + *, + name: str, + return_to: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[McpAuthStatus]: + """ + Returns current auth status. When OAuth is required, includes an authorization URL. Optional return_to is the post-consent landing path. + + Parameters + ---------- + name : str + MCP server name. + + return_to : typing.Optional[str] + Same-origin path to land in the browser after consent. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[McpAuthStatus] + Either already authenticated, or an authorization URL to redirect to. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}/authorize", + method="GET", + params={ + "return_to": return_to, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + McpAuthStatus, + construct_type( + type_=McpAuthStatus, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_authorization( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetMcpServerResponse]: + """ + Disconnects OAuth for the MCP server when applicable and returns the updated server with auth_status. No-op when the server does not use stored OAuth tokens. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMcpServerResponse] + The MCP server after disconnect. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}/authorize", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list_tools( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListMcpServerToolsResponse]: + """ + All tools exposed by the given MCP server (non-paginated), as returned by the MCP `tools/list` call. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListMcpServerToolsResponse] + All tools of the MCP server. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}/tools", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListMcpServerToolsResponse, + construct_type( + type_=ListMcpServerToolsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawMcpServersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListAvailableMcpServersResponse]: + """ + Configured MCP servers as a slim name/url list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListAvailableMcpServersResponse] + All configured MCP servers (chat projection). + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/mcp-servers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAvailableMcpServersResponse, + construct_type( + type_=ListAvailableMcpServersResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetAvailableMcpServerResponse]: + """ + A single MCP server as the slim chat projection, with live per-user auth_status. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetAvailableMcpServerResponse] + The MCP server (chat projection). + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetAvailableMcpServerResponse, + construct_type( + type_=GetAvailableMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def authorize( + self, + *, + name: str, + return_to: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[McpAuthStatus]: + """ + Returns current auth status. When OAuth is required, includes an authorization URL. Optional return_to is the post-consent landing path. + + Parameters + ---------- + name : str + MCP server name. + + return_to : typing.Optional[str] + Same-origin path to land in the browser after consent. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[McpAuthStatus] + Either already authenticated, or an authorization URL to redirect to. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}/authorize", + method="GET", + params={ + "return_to": return_to, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + McpAuthStatus, + construct_type( + type_=McpAuthStatus, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_authorization( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetMcpServerResponse]: + """ + Disconnects OAuth for the MCP server when applicable and returns the updated server with auth_status. No-op when the server does not use stored OAuth tokens. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMcpServerResponse] + The MCP server after disconnect. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}/authorize", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list_tools( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListMcpServerToolsResponse]: + """ + All tools exposed by the given MCP server (non-paginated), as returned by the MCP `tools/list` call. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListMcpServerToolsResponse] + All tools of the MCP server. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/mcp-servers/{encode_path_param(name)}/tools", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListMcpServerToolsResponse, + construct_type( + type_=ListMcpServerToolsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/models/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/models/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/models/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/models/client.py b/python/trueforge_sdk/src/trueforge_sdk/models/client.py new file mode 100644 index 000000000..b6e2390f6 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/models/client.py @@ -0,0 +1,102 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.list_available_models_response import ListAvailableModelsResponse +from .raw_client import AsyncRawModelsClient, RawModelsClient + + +class ModelsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawModelsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawModelsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawModelsClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAvailableModelsResponse: + """ + Configured models as a slim FQN list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAvailableModelsResponse + All configured models (chat projection). + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.models.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncModelsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawModelsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawModelsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawModelsClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAvailableModelsResponse: + """ + Configured models as a slim FQN list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAvailableModelsResponse + All configured models (chat projection). + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.models.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/models/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/models/raw_client.py new file mode 100644 index 000000000..3973cba4e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/models/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.unauthorized_error import UnauthorizedError +from ..types.list_available_models_response import ListAvailableModelsResponse +from ..types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawModelsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListAvailableModelsResponse]: + """ + Configured models as a slim FQN list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListAvailableModelsResponse] + All configured models (chat projection). + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/models", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAvailableModelsResponse, + construct_type( + type_=ListAvailableModelsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawModelsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListAvailableModelsResponse]: + """ + Configured models as a slim FQN list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListAvailableModelsResponse] + All configured models (chat projection). + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/models", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAvailableModelsResponse, + construct_type( + type_=ListAvailableModelsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/py.typed b/python/trueforge_sdk/src/trueforge_sdk/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/python/trueforge_sdk/src/trueforge_sdk/schedules/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/schedules/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/schedules/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/schedules/client.py b/python/trueforge_sdk/src/trueforge_sdk/schedules/client.py new file mode 100644 index 000000000..49f05994d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/schedules/client.py @@ -0,0 +1,705 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.create_schedule_run_response import CreateScheduleRunResponse +from ..types.delete_schedule_response import DeleteScheduleResponse +from ..types.get_schedule_response import GetScheduleResponse +from ..types.list_schedule_runs_response import ListScheduleRunsResponse +from ..types.list_schedules_response import ListSchedulesResponse +from ..types.resource_name import ResourceName +from ..types.schedule_manifest import ScheduleManifest +from .raw_client import AsyncRawSchedulesClient, RawSchedulesClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SchedulesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSchedulesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSchedulesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSchedulesClient + """ + return self._raw_client + + def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + agent_names: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListSchedulesResponse: + """ + List schedules for the tenant, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_names : typing.Optional[str] + Filter by one or more agent names (comma-separated). When set, at least one name is required. + + created_by_me : typing.Optional[bool] + When true, only schedules created by the authenticated subject. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSchedulesResponse + Paginated matching schedules. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.list() + """ + _response = self._raw_client.list( + limit=limit, + page_token=page_token, + agent_names=agent_names, + created_by_me=created_by_me, + request_options=request_options, + ) + return _response.data + + def create( + self, + *, + agent_name: ResourceName, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetScheduleResponse: + """ + Create a schedule for an existing agent (by name) and add its first pending run when active. + + Parameters + ---------- + agent_name : ResourceName + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScheduleResponse + Created schedule. + + Examples + -------- + from trueforge_sdk import ScheduleManifest, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.create( + agent_name="agent_name", + manifest=ScheduleManifest( + cron="cron", + task="task", + ), + name="name", + ) + """ + _response = self._raw_client.create( + agent_name=agent_name, manifest=manifest, name=name, request_options=request_options + ) + return _response.data + + def create_run( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> CreateScheduleRunResponse: + """ + Start a schedule run immediately using the schedule task. Does not replace or advance the cron pending run. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateScheduleRunResponse + Run created. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.create_run( + schedule_id="schedule_id", + ) + """ + _response = self._raw_client.create_run(schedule_id=schedule_id, request_options=request_options) + return _response.data + + def get(self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None) -> GetScheduleResponse: + """ + Get a schedule by id. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScheduleResponse + The schedule. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.get( + schedule_id="schedule_id", + ) + """ + _response = self._raw_client.get(schedule_id=schedule_id, request_options=request_options) + return _response.data + + def update( + self, + *, + schedule_id: str, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetScheduleResponse: + """ + Replace name and manifest; replaces or drops the pending run when status/cron/timezone change. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScheduleResponse + Updated schedule. + + Examples + -------- + from trueforge_sdk import ScheduleManifest, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.update( + schedule_id="schedule_id", + manifest=ScheduleManifest( + cron="cron", + task="task", + ), + name="name", + ) + """ + _response = self._raw_client.update( + schedule_id=schedule_id, manifest=manifest, name=name, request_options=request_options + ) + return _response.data + + def delete( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteScheduleResponse: + """ + Delete a schedule and its runs. Idempotent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteScheduleResponse + Deleted. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.delete( + schedule_id="schedule_id", + ) + """ + _response = self._raw_client.delete(schedule_id=schedule_id, request_options=request_options) + return _response.data + + def list_runs( + self, + *, + schedule_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListScheduleRunsResponse: + """ + List runs of a schedule, newest `scheduled_for` first. Available to its creator or a manager of its agent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListScheduleRunsResponse + Paginated runs of the schedule. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.schedules.list_runs( + schedule_id="schedule_id", + ) + """ + _response = self._raw_client.list_runs( + schedule_id=schedule_id, limit=limit, page_token=page_token, request_options=request_options + ) + return _response.data + + +class AsyncSchedulesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSchedulesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSchedulesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSchedulesClient + """ + return self._raw_client + + async def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + agent_names: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListSchedulesResponse: + """ + List schedules for the tenant, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_names : typing.Optional[str] + Filter by one or more agent names (comma-separated). When set, at least one name is required. + + created_by_me : typing.Optional[bool] + When true, only schedules created by the authenticated subject. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSchedulesResponse + Paginated matching schedules. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + limit=limit, + page_token=page_token, + agent_names=agent_names, + created_by_me=created_by_me, + request_options=request_options, + ) + return _response.data + + async def create( + self, + *, + agent_name: ResourceName, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetScheduleResponse: + """ + Create a schedule for an existing agent (by name) and add its first pending run when active. + + Parameters + ---------- + agent_name : ResourceName + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScheduleResponse + Created schedule. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, ScheduleManifest + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.create( + agent_name="agent_name", + manifest=ScheduleManifest( + cron="cron", + task="task", + ), + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + agent_name=agent_name, manifest=manifest, name=name, request_options=request_options + ) + return _response.data + + async def create_run( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> CreateScheduleRunResponse: + """ + Start a schedule run immediately using the schedule task. Does not replace or advance the cron pending run. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateScheduleRunResponse + Run created. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.create_run( + schedule_id="schedule_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_run(schedule_id=schedule_id, request_options=request_options) + return _response.data + + async def get( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetScheduleResponse: + """ + Get a schedule by id. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScheduleResponse + The schedule. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.get( + schedule_id="schedule_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(schedule_id=schedule_id, request_options=request_options) + return _response.data + + async def update( + self, + *, + schedule_id: str, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetScheduleResponse: + """ + Replace name and manifest; replaces or drops the pending run when status/cron/timezone change. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScheduleResponse + Updated schedule. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, ScheduleManifest + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.update( + schedule_id="schedule_id", + manifest=ScheduleManifest( + cron="cron", + task="task", + ), + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + schedule_id=schedule_id, manifest=manifest, name=name, request_options=request_options + ) + return _response.data + + async def delete( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteScheduleResponse: + """ + Delete a schedule and its runs. Idempotent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteScheduleResponse + Deleted. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.delete( + schedule_id="schedule_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(schedule_id=schedule_id, request_options=request_options) + return _response.data + + async def list_runs( + self, + *, + schedule_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListScheduleRunsResponse: + """ + List runs of a schedule, newest `scheduled_for` first. Available to its creator or a manager of its agent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListScheduleRunsResponse + Paginated runs of the schedule. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.schedules.list_runs( + schedule_id="schedule_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_runs( + schedule_id=schedule_id, limit=limit, page_token=page_token, request_options=request_options + ) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/schedules/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/schedules/raw_client.py new file mode 100644 index 000000000..a227ae6f8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/schedules/raw_client.py @@ -0,0 +1,1267 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.forbidden_error import ForbiddenError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.create_schedule_run_response import CreateScheduleRunResponse +from ..types.delete_schedule_response import DeleteScheduleResponse +from ..types.get_schedule_response import GetScheduleResponse +from ..types.list_schedule_runs_response import ListScheduleRunsResponse +from ..types.list_schedules_response import ListSchedulesResponse +from ..types.request_error_response import RequestErrorResponse +from ..types.resource_name import ResourceName +from ..types.schedule_manifest import ScheduleManifest +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSchedulesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + agent_names: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListSchedulesResponse]: + """ + List schedules for the tenant, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_names : typing.Optional[str] + Filter by one or more agent names (comma-separated). When set, at least one name is required. + + created_by_me : typing.Optional[bool] + When true, only schedules created by the authenticated subject. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListSchedulesResponse] + Paginated matching schedules. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/schedules", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + "agent_names": agent_names, + "created_by_me": created_by_me, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSchedulesResponse, + construct_type( + type_=ListSchedulesResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, + *, + agent_name: ResourceName, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetScheduleResponse]: + """ + Create a schedule for an existing agent (by name) and add its first pending run when active. + + Parameters + ---------- + agent_name : ResourceName + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetScheduleResponse] + Created schedule. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/schedules", + method="POST", + json={ + "agent_name": agent_name, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ScheduleManifest, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScheduleResponse, + construct_type( + type_=GetScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_run( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[CreateScheduleRunResponse]: + """ + Start a schedule run immediately using the schedule task. Does not replace or advance the cron pending run. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreateScheduleRunResponse] + Run created. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/schedules/runs", + method="POST", + json={ + "schedule_id": schedule_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateScheduleRunResponse, + construct_type( + type_=CreateScheduleRunResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetScheduleResponse]: + """ + Get a schedule by id. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetScheduleResponse] + The schedule. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScheduleResponse, + construct_type( + type_=GetScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + *, + schedule_id: str, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetScheduleResponse]: + """ + Replace name and manifest; replaces or drops the pending run when status/cron/timezone change. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetScheduleResponse] + Updated schedule. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ScheduleManifest, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScheduleResponse, + construct_type( + type_=GetScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DeleteScheduleResponse]: + """ + Delete a schedule and its runs. Idempotent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteScheduleResponse] + Deleted. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteScheduleResponse, + construct_type( + type_=DeleteScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list_runs( + self, + *, + schedule_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListScheduleRunsResponse]: + """ + List runs of a schedule, newest `scheduled_for` first. Available to its creator or a manager of its agent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListScheduleRunsResponse] + Paginated runs of the schedule. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}/runs", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListScheduleRunsResponse, + construct_type( + type_=ListScheduleRunsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSchedulesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + agent_names: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListSchedulesResponse]: + """ + List schedules for the tenant, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + agent_names : typing.Optional[str] + Filter by one or more agent names (comma-separated). When set, at least one name is required. + + created_by_me : typing.Optional[bool] + When true, only schedules created by the authenticated subject. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListSchedulesResponse] + Paginated matching schedules. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/schedules", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + "agent_names": agent_names, + "created_by_me": created_by_me, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSchedulesResponse, + construct_type( + type_=ListSchedulesResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, + *, + agent_name: ResourceName, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetScheduleResponse]: + """ + Create a schedule for an existing agent (by name) and add its first pending run when active. + + Parameters + ---------- + agent_name : ResourceName + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetScheduleResponse] + Created schedule. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/schedules", + method="POST", + json={ + "agent_name": agent_name, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ScheduleManifest, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScheduleResponse, + construct_type( + type_=GetScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_run( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[CreateScheduleRunResponse]: + """ + Start a schedule run immediately using the schedule task. Does not replace or advance the cron pending run. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreateScheduleRunResponse] + Run created. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/schedules/runs", + method="POST", + json={ + "schedule_id": schedule_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateScheduleRunResponse, + construct_type( + type_=CreateScheduleRunResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetScheduleResponse]: + """ + Get a schedule by id. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetScheduleResponse] + The schedule. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScheduleResponse, + construct_type( + type_=GetScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + *, + schedule_id: str, + manifest: ScheduleManifest, + name: ResourceName, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetScheduleResponse]: + """ + Replace name and manifest; replaces or drops the pending run when status/cron/timezone change. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + manifest : ScheduleManifest + + name : ResourceName + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetScheduleResponse] + Updated schedule. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ScheduleManifest, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScheduleResponse, + construct_type( + type_=GetScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, *, schedule_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DeleteScheduleResponse]: + """ + Delete a schedule and its runs. Idempotent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteScheduleResponse] + Deleted. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteScheduleResponse, + construct_type( + type_=DeleteScheduleResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list_runs( + self, + *, + schedule_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListScheduleRunsResponse]: + """ + List runs of a schedule, newest `scheduled_for` first. Available to its creator or a manager of its agent. + + Parameters + ---------- + schedule_id : str + Immutable schedule identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListScheduleRunsResponse] + Paginated runs of the schedule. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/schedules/{encode_path_param(schedule_id)}/runs", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListScheduleRunsResponse, + construct_type( + type_=ListScheduleRunsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/server/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/server/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/server/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/server/client.py b/python/trueforge_sdk/src/trueforge_sdk/server/client.py new file mode 100644 index 000000000..413d9811c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/server/client.py @@ -0,0 +1,104 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.get_capabilities_response import GetCapabilitiesResponse +from .raw_client import AsyncRawServerClient, RawServerClient + + +class ServerClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawServerClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawServerClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawServerClient + """ + return self._raw_client + + def get_capabilities(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetCapabilitiesResponse: + """ + Report optional runtime capabilities available for this tenant. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetCapabilitiesResponse + Server capabilities. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.server.get_capabilities() + """ + _response = self._raw_client.get_capabilities(request_options=request_options) + return _response.data + + +class AsyncServerClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawServerClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawServerClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawServerClient + """ + return self._raw_client + + async def get_capabilities( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> GetCapabilitiesResponse: + """ + Report optional runtime capabilities available for this tenant. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetCapabilitiesResponse + Server capabilities. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.server.get_capabilities() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_capabilities(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/server/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/server/raw_client.py new file mode 100644 index 000000000..3456e7962 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/server/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.unauthorized_error import UnauthorizedError +from ..types.get_capabilities_response import GetCapabilitiesResponse +from ..types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawServerClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_capabilities( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetCapabilitiesResponse]: + """ + Report optional runtime capabilities available for this tenant. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetCapabilitiesResponse] + Server capabilities. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/capabilities", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetCapabilitiesResponse, + construct_type( + type_=GetCapabilitiesResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawServerClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_capabilities( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetCapabilitiesResponse]: + """ + Report optional runtime capabilities available for this tenant. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetCapabilitiesResponse] + Server capabilities. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/capabilities", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetCapabilitiesResponse, + construct_type( + type_=GetCapabilitiesResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py new file mode 100644 index 000000000..6f1726ffa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py @@ -0,0 +1,1527 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..core.stream import AsyncStream, Stream, StreamEvent +from ..types.cancel_session_response import CancelSessionResponse +from ..types.create_session_agent import CreateSessionAgent +from ..types.get_session_response import GetSessionResponse +from ..types.get_turn_response import GetTurnResponse +from ..types.list_session_events_response import ListSessionEventsResponse +from ..types.list_sessions_order import ListSessionsOrder +from ..types.list_sessions_response import ListSessionsResponse +from ..types.list_turn_events_order import ListTurnEventsOrder +from ..types.list_turn_events_response import ListTurnEventsResponse +from ..types.list_turns_response import ListTurnsResponse +from ..types.previous_turn_id_input import PreviousTurnIdInput +from ..types.session_agent_spec_body import SessionAgentSpecBody +from ..types.session_metadata import SessionMetadata +from ..types.session_source_type import SessionSourceType +from ..types.turn_input_item import TurnInputItem +from ..types.turn_streaming_event import TurnStreamingEvent +from .raw_client import AsyncRawSessionsClient, RawSessionsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSessionsClient + """ + return self._raw_client + + def list( + self, + *, + limit: typing.Optional[int] = 25, + order: typing.Optional[ListSessionsOrder] = None, + page_token: typing.Optional[str] = None, + start_timestamp: typing.Optional[dt.datetime] = None, + end_timestamp: typing.Optional[dt.datetime] = None, + agent_id: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + metadata: typing.Optional[SessionMetadata] = None, + source_type: typing.Optional[SessionSourceType] = None, + source_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListSessionsResponse: + """ + List the sessions (newest first by default). + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + order : typing.Optional[ListSessionsOrder] + Sort sessions by `updated_at`. Defaults to "desc". + + page_token : typing.Optional[str] + Opaque keyset cursor from a previous response `next_page_token`. + + start_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on `created_at` (ISO-8601 / RFC 3339). + + end_timestamp : typing.Optional[dt.datetime] + Inclusive upper bound on `created_at` (ISO-8601 / RFC 3339). + + agent_id : typing.Optional[str] + When set, only sessions bound to this agent id are returned. + + created_by_me : typing.Optional[bool] + When true, only sessions created by the authenticated subject. + + metadata : typing.Optional[SessionMetadata] + Exact metadata pairs as metadata[key]=value. Sessions must contain all pairs. + + source_type : typing.Optional[SessionSourceType] + When set, returns only sessions created by this source type. + + source_id : typing.Optional[str] + When set, returns only sessions from this specific source. Requires source_type. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSessionsResponse + Paginated sessions. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.list() + """ + _response = self._raw_client.list( + limit=limit, + order=order, + page_token=page_token, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + agent_id=agent_id, + created_by_me=created_by_me, + metadata=metadata, + source_type=source_type, + source_id=source_id, + request_options=request_options, + ) + return _response.data + + def create( + self, + *, + agent: CreateSessionAgent, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionResponse: + """ + Create a session with `agent` as either `{ name }` (named registry binding) or `{ spec: AgentSpec }` (inline). Named sessions snapshot the agent name at create and resolve the live agent on each turn. Responses use `{ type: "reference", name, id }` or `{ type: "inline", spec }`. + + Parameters + ---------- + agent : CreateSessionAgent + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session created. + + Examples + -------- + from trueforge_sdk import SessionAgentNameRef, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.create( + agent=SessionAgentNameRef( + name="name", + ), + ) + """ + _response = self._raw_client.create(agent=agent, metadata=metadata, request_options=request_options) + return _response.data + + def get(self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None) -> GetSessionResponse: + """ + Fetch a session by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session data. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.get( + session_id="session_id", + ) + """ + _response = self._raw_client.get(session_id=session_id, request_options=request_options) + return _response.data + + def delete(self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Delete a session and all related turns, events, and internal state. Only the session creator may delete it. Idempotent if already gone. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.delete( + session_id="session_id", + ) + """ + _response = self._raw_client.delete(session_id=session_id, request_options=request_options) + return _response.data + + def update( + self, + *, + session_id: str, + agent: typing.Optional[SessionAgentSpecBody] = OMIT, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionResponse: + """ + Update a session by replacing `agent` with `{ spec: AgentSpec }`. Named (reference) sessions reject agent updates. An empty body is a valid no-op that refreshes `updated_at`. Only the session creator may update it. + + Parameters + ---------- + session_id : str + Session identifier. + + agent : typing.Optional[SessionAgentSpecBody] + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session updated. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.update( + session_id="session_id", + ) + """ + _response = self._raw_client.update( + session_id=session_id, agent=agent, metadata=metadata, request_options=request_options + ) + return _response.data + + def cancel( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> CancelSessionResponse: + """ + Cancel the running last turn for a session. Only the session creator may cancel. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CancelSessionResponse + Turn cancelled. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.cancel( + session_id="session_id", + ) + """ + _response = self._raw_client.cancel(session_id=session_id, request_options=request_options) + return _response.data + + def list_events( + self, + *, + session_id: str, + page_token: typing.Optional[str] = None, + last_turn_id: typing.Optional[str] = None, + limit: typing.Optional[int] = 100, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListSessionEventsResponse: + """ + List session events as `{ turn_id, event }` across the active turn branch (newest first), including persisted events from a running tip. Each turn contributes turn.created, content events (model.message, tool.call, …), and turn.done when terminal; streaming deltas are not included. Use `page_token` to paginate backward toward older events while retaining the original branch anchor. Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + page_token : typing.Optional[str] + Pagination cursor from `pagination.next_page_token`. It retains the branch anchor turn and returns older events toward the session start. + + last_turn_id : typing.Optional[str] + Newest turn in the listing window (initial load only; ignored when `page_token` is set). Lists that turn and its ancestors, newest events first. Omit to use the session last turn. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSessionEventsResponse + Paginated session events. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.list_events( + session_id="session_id", + ) + """ + _response = self._raw_client.list_events( + session_id=session_id, + page_token=page_token, + last_turn_id=last_turn_id, + limit=limit, + request_options=request_options, + ) + return _response.data + + def list_turns( + self, + *, + session_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListTurnsResponse: + """ + List turns for a session (newest first by default), token-paginated. Only the session creator may list turns. + + Parameters + ---------- + session_id : str + Session identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListTurnsResponse + Paginated turns. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.list_turns( + session_id="session_id", + ) + """ + _response = self._raw_client.list_turns( + session_id=session_id, limit=limit, page_token=page_token, request_options=request_options + ) + return _response.data + + def create_turn_stream( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Stream[TurnStreamingEvent]: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Stream[TurnStreamingEvent] + + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + response = client.sessions.create_turn_stream( + session_id="session_id", + ) + for chunk in response: + yield chunk + """ + + def _events() -> typing.Generator[StreamEvent[TurnStreamingEvent], None, None]: + with self._raw_client.create_turn_stream( + session_id=session_id, input=input, previous_turn_id=previous_turn_id, request_options=request_options + ) as r: + yield from r.data.with_metadata() + + return Stream(events=_events) + + def create_turn( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetTurnResponse: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetTurnResponse + + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.create_turn( + session_id="session_id", + ) + """ + _response = self._raw_client.create_turn( + session_id=session_id, input=input, previous_turn_id=previous_turn_id, request_options=request_options + ) + return _response.data + + def get_turn( + self, *, session_id: str, turn_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetTurnResponse: + """ + Fetch a single turn by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetTurnResponse + Turn data. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.get_turn( + session_id="session_id", + turn_id="turn_id", + ) + """ + _response = self._raw_client.get_turn(session_id=session_id, turn_id=turn_id, request_options=request_options) + return _response.data + + def download_sandbox_file( + self, *, session_id: str, turn_id: str, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Iterator[bytes]: + """ + Download a file from the sandbox this turn ran in. Paths come from the assistant's `sandbox_artifacts` block. Only the session creator may download. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + path : str + Absolute or sandbox-working-directory-relative file path, as listed in the assistant's `sandbox_artifacts` block. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[bytes] + File contents. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.download_sandbox_file( + session_id="session_id", + turn_id="turn_id", + path="x", + ) + """ + with self._raw_client.download_sandbox_file( + session_id=session_id, turn_id=turn_id, path=path, request_options=request_options + ) as r: + yield from r.data + + def list_turn_events( + self, + *, + session_id: str, + turn_id: str, + limit: typing.Optional[int] = 100, + page_token: typing.Optional[str] = None, + order: typing.Optional[ListTurnEventsOrder] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListTurnEventsResponse: + """ + Paginated persisted events for a turn (insertion order by default). Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + order : typing.Optional[ListTurnEventsOrder] + Sort events by insertion order. Defaults to "asc". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListTurnEventsResponse + Paginated turn events. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.list_turn_events( + session_id="session_id", + turn_id="turn_id", + ) + """ + _response = self._raw_client.list_turn_events( + session_id=session_id, + turn_id=turn_id, + limit=limit, + page_token=page_token, + order=order, + request_options=request_options, + ) + return _response.data + + def subscribe_to_turn( + self, + *, + session_id: str, + turn_id: str, + after_sequence_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Stream[TurnStreamingEvent]: + """ + Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + after_sequence_number : typing.Optional[int] + Exclusive resume cursor: replay only events with a sequence number greater than this value. Omit to start from the beginning of the live buffer. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Stream[TurnStreamingEvent] + Server-Sent Events stream of turn events (deltas and lifecycle). + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + response = client.sessions.subscribe_to_turn( + session_id="session_id", + turn_id="turn_id", + ) + for chunk in response: + yield chunk + """ + + def _events() -> typing.Generator[StreamEvent[TurnStreamingEvent], None, None]: + with self._raw_client.subscribe_to_turn( + session_id=session_id, + turn_id=turn_id, + after_sequence_number=after_sequence_number, + request_options=request_options, + ) as r: + yield from r.data.with_metadata() + + return Stream(events=_events) + + +class AsyncSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSessionsClient + """ + return self._raw_client + + async def list( + self, + *, + limit: typing.Optional[int] = 25, + order: typing.Optional[ListSessionsOrder] = None, + page_token: typing.Optional[str] = None, + start_timestamp: typing.Optional[dt.datetime] = None, + end_timestamp: typing.Optional[dt.datetime] = None, + agent_id: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + metadata: typing.Optional[SessionMetadata] = None, + source_type: typing.Optional[SessionSourceType] = None, + source_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListSessionsResponse: + """ + List the sessions (newest first by default). + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + order : typing.Optional[ListSessionsOrder] + Sort sessions by `updated_at`. Defaults to "desc". + + page_token : typing.Optional[str] + Opaque keyset cursor from a previous response `next_page_token`. + + start_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on `created_at` (ISO-8601 / RFC 3339). + + end_timestamp : typing.Optional[dt.datetime] + Inclusive upper bound on `created_at` (ISO-8601 / RFC 3339). + + agent_id : typing.Optional[str] + When set, only sessions bound to this agent id are returned. + + created_by_me : typing.Optional[bool] + When true, only sessions created by the authenticated subject. + + metadata : typing.Optional[SessionMetadata] + Exact metadata pairs as metadata[key]=value. Sessions must contain all pairs. + + source_type : typing.Optional[SessionSourceType] + When set, returns only sessions created by this source type. + + source_id : typing.Optional[str] + When set, returns only sessions from this specific source. Requires source_type. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSessionsResponse + Paginated sessions. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + limit=limit, + order=order, + page_token=page_token, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + agent_id=agent_id, + created_by_me=created_by_me, + metadata=metadata, + source_type=source_type, + source_id=source_id, + request_options=request_options, + ) + return _response.data + + async def create( + self, + *, + agent: CreateSessionAgent, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionResponse: + """ + Create a session with `agent` as either `{ name }` (named registry binding) or `{ spec: AgentSpec }` (inline). Named sessions snapshot the agent name at create and resolve the live agent on each turn. Responses use `{ type: "reference", name, id }` or `{ type: "inline", spec }`. + + Parameters + ---------- + agent : CreateSessionAgent + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session created. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, SessionAgentNameRef + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.create( + agent=SessionAgentNameRef( + name="name", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(agent=agent, metadata=metadata, request_options=request_options) + return _response.data + + async def get( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetSessionResponse: + """ + Fetch a session by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session data. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.get( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(session_id=session_id, request_options=request_options) + return _response.data + + async def delete(self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Delete a session and all related turns, events, and internal state. Only the session creator may delete it. Idempotent if already gone. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.delete( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(session_id=session_id, request_options=request_options) + return _response.data + + async def update( + self, + *, + session_id: str, + agent: typing.Optional[SessionAgentSpecBody] = OMIT, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSessionResponse: + """ + Update a session by replacing `agent` with `{ spec: AgentSpec }`. Named (reference) sessions reject agent updates. An empty body is a valid no-op that refreshes `updated_at`. Only the session creator may update it. + + Parameters + ---------- + session_id : str + Session identifier. + + agent : typing.Optional[SessionAgentSpecBody] + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSessionResponse + Session updated. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.update( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + session_id=session_id, agent=agent, metadata=metadata, request_options=request_options + ) + return _response.data + + async def cancel( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> CancelSessionResponse: + """ + Cancel the running last turn for a session. Only the session creator may cancel. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CancelSessionResponse + Turn cancelled. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.cancel( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.cancel(session_id=session_id, request_options=request_options) + return _response.data + + async def list_events( + self, + *, + session_id: str, + page_token: typing.Optional[str] = None, + last_turn_id: typing.Optional[str] = None, + limit: typing.Optional[int] = 100, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListSessionEventsResponse: + """ + List session events as `{ turn_id, event }` across the active turn branch (newest first), including persisted events from a running tip. Each turn contributes turn.created, content events (model.message, tool.call, …), and turn.done when terminal; streaming deltas are not included. Use `page_token` to paginate backward toward older events while retaining the original branch anchor. Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + page_token : typing.Optional[str] + Pagination cursor from `pagination.next_page_token`. It retains the branch anchor turn and returns older events toward the session start. + + last_turn_id : typing.Optional[str] + Newest turn in the listing window (initial load only; ignored when `page_token` is set). Lists that turn and its ancestors, newest events first. Omit to use the session last turn. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSessionEventsResponse + Paginated session events. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.list_events( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_events( + session_id=session_id, + page_token=page_token, + last_turn_id=last_turn_id, + limit=limit, + request_options=request_options, + ) + return _response.data + + async def list_turns( + self, + *, + session_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListTurnsResponse: + """ + List turns for a session (newest first by default), token-paginated. Only the session creator may list turns. + + Parameters + ---------- + session_id : str + Session identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListTurnsResponse + Paginated turns. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.list_turns( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_turns( + session_id=session_id, limit=limit, page_token=page_token, request_options=request_options + ) + return _response.data + + def create_turn_stream( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncStream[TurnStreamingEvent]: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncStream[TurnStreamingEvent] + + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + response = await client.sessions.create_turn_stream( + session_id="session_id", + ) + async for chunk in response: + yield chunk + + + asyncio.run(main()) + """ + + async def _events() -> typing.AsyncGenerator[StreamEvent[TurnStreamingEvent], None]: + async with self._raw_client.create_turn_stream( + session_id=session_id, input=input, previous_turn_id=previous_turn_id, request_options=request_options + ) as r: + async for _event in r.data.with_metadata(): + yield _event + + return AsyncStream(events=_events) + + async def create_turn( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetTurnResponse: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetTurnResponse + + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.create_turn( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_turn( + session_id=session_id, input=input, previous_turn_id=previous_turn_id, request_options=request_options + ) + return _response.data + + async def get_turn( + self, *, session_id: str, turn_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetTurnResponse: + """ + Fetch a single turn by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetTurnResponse + Turn data. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.get_turn( + session_id="session_id", + turn_id="turn_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_turn( + session_id=session_id, turn_id=turn_id, request_options=request_options + ) + return _response.data + + async def download_sandbox_file( + self, *, session_id: str, turn_id: str, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.AsyncIterator[bytes]: + """ + Download a file from the sandbox this turn ran in. Paths come from the assistant's `sandbox_artifacts` block. Only the session creator may download. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + path : str + Absolute or sandbox-working-directory-relative file path, as listed in the assistant's `sandbox_artifacts` block. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[bytes] + File contents. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.download_sandbox_file( + session_id="session_id", + turn_id="turn_id", + path="x", + ) + + + asyncio.run(main()) + """ + async with self._raw_client.download_sandbox_file( + session_id=session_id, turn_id=turn_id, path=path, request_options=request_options + ) as r: + async for _chunk in r.data: + yield _chunk + + async def list_turn_events( + self, + *, + session_id: str, + turn_id: str, + limit: typing.Optional[int] = 100, + page_token: typing.Optional[str] = None, + order: typing.Optional[ListTurnEventsOrder] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListTurnEventsResponse: + """ + Paginated persisted events for a turn (insertion order by default). Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + order : typing.Optional[ListTurnEventsOrder] + Sort events by insertion order. Defaults to "asc". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListTurnEventsResponse + Paginated turn events. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.list_turn_events( + session_id="session_id", + turn_id="turn_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_turn_events( + session_id=session_id, + turn_id=turn_id, + limit=limit, + page_token=page_token, + order=order, + request_options=request_options, + ) + return _response.data + + def subscribe_to_turn( + self, + *, + session_id: str, + turn_id: str, + after_sequence_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncStream[TurnStreamingEvent]: + """ + Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + after_sequence_number : typing.Optional[int] + Exclusive resume cursor: replay only events with a sequence number greater than this value. Omit to start from the beginning of the live buffer. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncStream[TurnStreamingEvent] + Server-Sent Events stream of turn events (deltas and lifecycle). + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + response = await client.sessions.subscribe_to_turn( + session_id="session_id", + turn_id="turn_id", + ) + async for chunk in response: + yield chunk + + + asyncio.run(main()) + """ + + async def _events() -> typing.AsyncGenerator[StreamEvent[TurnStreamingEvent], None]: + async with self._raw_client.subscribe_to_turn( + session_id=session_id, + turn_id=turn_id, + after_sequence_number=after_sequence_number, + request_options=request_options, + ) as r: + async for _event in r.data.with_metadata(): + yield _event + + return AsyncStream(events=_events) diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py new file mode 100644 index 000000000..adbb14122 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py @@ -0,0 +1,2967 @@ +# This file was auto-generated by Fern from our API Definition. + +import contextlib +import datetime as dt +import typing +from json.decoder import JSONDecodeError +from logging import error, warning + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.http_sse._api import EventSource +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_sse_obj +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.stream import AsyncStream, Stream, StreamEvent +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.content_too_large_error import ContentTooLargeError +from ..errors.failed_dependency_error import FailedDependencyError +from ..errors.forbidden_error import ForbiddenError +from ..errors.gone_error import GoneError +from ..errors.not_found_error import NotFoundError +from ..errors.precondition_failed_error import PreconditionFailedError +from ..errors.unauthorized_error import UnauthorizedError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.cancel_session_response import CancelSessionResponse +from ..types.create_session_agent import CreateSessionAgent +from ..types.get_session_response import GetSessionResponse +from ..types.get_turn_response import GetTurnResponse +from ..types.list_session_events_response import ListSessionEventsResponse +from ..types.list_sessions_order import ListSessionsOrder +from ..types.list_sessions_response import ListSessionsResponse +from ..types.list_turn_events_order import ListTurnEventsOrder +from ..types.list_turn_events_response import ListTurnEventsResponse +from ..types.list_turns_response import ListTurnsResponse +from ..types.previous_turn_id_input import PreviousTurnIdInput +from ..types.request_error_response import RequestErrorResponse +from ..types.session_agent_spec_body import SessionAgentSpecBody +from ..types.session_metadata import SessionMetadata +from ..types.session_source_type import SessionSourceType +from ..types.turn_input_item import TurnInputItem +from ..types.turn_streaming_event import TurnStreamingEvent +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + limit: typing.Optional[int] = 25, + order: typing.Optional[ListSessionsOrder] = None, + page_token: typing.Optional[str] = None, + start_timestamp: typing.Optional[dt.datetime] = None, + end_timestamp: typing.Optional[dt.datetime] = None, + agent_id: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + metadata: typing.Optional[SessionMetadata] = None, + source_type: typing.Optional[SessionSourceType] = None, + source_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListSessionsResponse]: + """ + List the sessions (newest first by default). + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + order : typing.Optional[ListSessionsOrder] + Sort sessions by `updated_at`. Defaults to "desc". + + page_token : typing.Optional[str] + Opaque keyset cursor from a previous response `next_page_token`. + + start_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on `created_at` (ISO-8601 / RFC 3339). + + end_timestamp : typing.Optional[dt.datetime] + Inclusive upper bound on `created_at` (ISO-8601 / RFC 3339). + + agent_id : typing.Optional[str] + When set, only sessions bound to this agent id are returned. + + created_by_me : typing.Optional[bool] + When true, only sessions created by the authenticated subject. + + metadata : typing.Optional[SessionMetadata] + Exact metadata pairs as metadata[key]=value. Sessions must contain all pairs. + + source_type : typing.Optional[SessionSourceType] + When set, returns only sessions created by this source type. + + source_id : typing.Optional[str] + When set, returns only sessions from this specific source. Requires source_type. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListSessionsResponse] + Paginated sessions. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/sessions", + method="GET", + params={ + "limit": limit, + "order": order, + "page_token": page_token, + "start_timestamp": serialize_datetime(start_timestamp) if start_timestamp is not None else None, + "end_timestamp": serialize_datetime(end_timestamp) if end_timestamp is not None else None, + "agent_id": agent_id, + "created_by_me": created_by_me, + "metadata": metadata, + "source_type": source_type, + "source_id": source_id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSessionsResponse, + construct_type( + type_=ListSessionsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, + *, + agent: CreateSessionAgent, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSessionResponse]: + """ + Create a session with `agent` as either `{ name }` (named registry binding) or `{ spec: AgentSpec }` (inline). Named sessions snapshot the agent name at create and resolve the live agent on each turn. Responses use `{ type: "reference", name, id }` or `{ type: "inline", spec }`. + + Parameters + ---------- + agent : CreateSessionAgent + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionResponse] + Session created. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/sessions", + method="POST", + json={ + "agent": convert_and_respect_annotation_metadata( + object_=agent, annotation=CreateSessionAgent, direction="write" + ), + "metadata": metadata, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSessionResponse]: + """ + Fetch a session by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionResponse] + Session data. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete(self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]: + """ + Delete a session and all related turns, events, and internal state. Only the session creator may delete it. Idempotent if already gone. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + *, + session_id: str, + agent: typing.Optional[SessionAgentSpecBody] = OMIT, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSessionResponse]: + """ + Update a session by replacing `agent` with `{ spec: AgentSpec }`. Named (reference) sessions reject agent updates. An empty body is a valid no-op that refreshes `updated_at`. Only the session creator may update it. + + Parameters + ---------- + session_id : str + Session identifier. + + agent : typing.Optional[SessionAgentSpecBody] + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSessionResponse] + Session updated. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}", + method="PATCH", + json={ + "agent": convert_and_respect_annotation_metadata( + object_=agent, annotation=SessionAgentSpecBody, direction="write" + ), + "metadata": metadata, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def cancel( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[CancelSessionResponse]: + """ + Cancel the running last turn for a session. Only the session creator may cancel. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CancelSessionResponse] + Turn cancelled. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/cancel", + method="POST", + json={}, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CancelSessionResponse, + construct_type( + type_=CancelSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list_events( + self, + *, + session_id: str, + page_token: typing.Optional[str] = None, + last_turn_id: typing.Optional[str] = None, + limit: typing.Optional[int] = 100, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListSessionEventsResponse]: + """ + List session events as `{ turn_id, event }` across the active turn branch (newest first), including persisted events from a running tip. Each turn contributes turn.created, content events (model.message, tool.call, …), and turn.done when terminal; streaming deltas are not included. Use `page_token` to paginate backward toward older events while retaining the original branch anchor. Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + page_token : typing.Optional[str] + Pagination cursor from `pagination.next_page_token`. It retains the branch anchor turn and returns older events toward the session start. + + last_turn_id : typing.Optional[str] + Newest turn in the listing window (initial load only; ignored when `page_token` is set). Lists that turn and its ancestors, newest events first. Omit to use the session last turn. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListSessionEventsResponse] + Paginated session events. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/events", + method="GET", + params={ + "page_token": page_token, + "last_turn_id": last_turn_id, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSessionEventsResponse, + construct_type( + type_=ListSessionEventsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list_turns( + self, + *, + session_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListTurnsResponse]: + """ + List turns for a session (newest first by default), token-paginated. Only the session creator may list turns. + + Parameters + ---------- + session_id : str + Session identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListTurnsResponse] + Paginated turns. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListTurnsResponse, + construct_type( + type_=ListTurnsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.contextmanager + def create_turn_stream( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[HttpResponse[Stream[TurnStreamingEvent]]]: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[HttpResponse[Stream[TurnStreamingEvent]]] + + """ + with self._client_wrapper.httpx_client.stream( + f"api/v1/sessions/{encode_path_param(session_id)}/turns", + method="POST", + json={ + "input": convert_and_respect_annotation_metadata( + object_=input, annotation=typing.Sequence[TurnInputItem], direction="write" + ), + "previous_turn_id": convert_and_respect_annotation_metadata( + object_=previous_turn_id, annotation=PreviousTurnIdInput, direction="write" + ), + "stream": True, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + def _stream() -> HttpResponse[Stream[TurnStreamingEvent]]: + try: + if 200 <= _response.status_code < 300: + + def _iter() -> typing.Generator[StreamEvent[TurnStreamingEvent], None, None]: + _event_source = EventSource(_response) + for _sse in _event_source.iter_sse(): + if _sse.data == None: + return + if len(_sse.data) == 0: + continue + try: + yield StreamEvent( + data=typing.cast( + TurnStreamingEvent, + parse_sse_obj( + sse=_sse, + type_=TurnStreamingEvent, # type: ignore + ), + ), + id=_sse.id or None, + event=_sse.event, + retry=_sse.retry, + ) + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + return + + return HttpResponse(response=_response, data=Stream(events=_iter)) + _response.read() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + + def create_turn( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetTurnResponse]: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetTurnResponse] + + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns", + method="POST", + json={ + "input": convert_and_respect_annotation_metadata( + object_=input, annotation=typing.Sequence[TurnInputItem], direction="write" + ), + "previous_turn_id": convert_and_respect_annotation_metadata( + object_=previous_turn_id, annotation=PreviousTurnIdInput, direction="write" + ), + "stream": False, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetTurnResponse, + construct_type( + type_=GetTurnResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_turn( + self, *, session_id: str, turn_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetTurnResponse]: + """ + Fetch a single turn by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetTurnResponse] + Turn data. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetTurnResponse, + construct_type( + type_=GetTurnResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.contextmanager + def download_sandbox_file( + self, *, session_id: str, turn_id: str, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: + """ + Download a file from the sandbox this turn ran in. Paths come from the assistant's `sandbox_artifacts` block. Only the session creator may download. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + path : str + Absolute or sandbox-working-directory-relative file path, as listed in the assistant's `sandbox_artifacts` block. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[HttpResponse[typing.Iterator[bytes]]] + File contents. + """ + with self._client_wrapper.httpx_client.stream( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/download-sandbox-file", + method="GET", + params={ + "path": path, + }, + request_options=request_options, + ) as _response: + + def _stream() -> HttpResponse[typing.Iterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return HttpResponse( + response=_response, data=(_chunk for _chunk in _response.iter_bytes(chunk_size=_chunk_size)) + ) + _response.read() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 410: + raise GoneError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + + def list_turn_events( + self, + *, + session_id: str, + turn_id: str, + limit: typing.Optional[int] = 100, + page_token: typing.Optional[str] = None, + order: typing.Optional[ListTurnEventsOrder] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListTurnEventsResponse]: + """ + Paginated persisted events for a turn (insertion order by default). Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + order : typing.Optional[ListTurnEventsOrder] + Sort events by insertion order. Defaults to "asc". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListTurnEventsResponse] + Paginated turn events. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + "order": order, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListTurnEventsResponse, + construct_type( + type_=ListTurnEventsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.contextmanager + def subscribe_to_turn( + self, + *, + session_id: str, + turn_id: str, + after_sequence_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[HttpResponse[Stream[TurnStreamingEvent]]]: + """ + Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + after_sequence_number : typing.Optional[int] + Exclusive resume cursor: replay only events with a sequence number greater than this value. Omit to start from the beginning of the live buffer. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[HttpResponse[Stream[TurnStreamingEvent]]] + Server-Sent Events stream of turn events (deltas and lifecycle). + """ + with self._client_wrapper.httpx_client.stream( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/subscribe", + method="GET", + params={ + "after_sequence_number": after_sequence_number, + }, + request_options=request_options, + ) as _response: + + def _stream() -> HttpResponse[Stream[TurnStreamingEvent]]: + try: + if 200 <= _response.status_code < 300: + + def _iter() -> typing.Generator[StreamEvent[TurnStreamingEvent], None, None]: + _event_source = EventSource(_response) + for _sse in _event_source.iter_sse(): + if _sse.data == None: + return + if len(_sse.data) == 0: + continue + try: + yield StreamEvent( + data=typing.cast( + TurnStreamingEvent, + parse_sse_obj( + sse=_sse, + type_=TurnStreamingEvent, # type: ignore + ), + ), + id=_sse.id or None, + event=_sse.event, + retry=_sse.retry, + ) + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + return + + return HttpResponse(response=_response, data=Stream(events=_iter)) + _response.read() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + + +class AsyncRawSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + limit: typing.Optional[int] = 25, + order: typing.Optional[ListSessionsOrder] = None, + page_token: typing.Optional[str] = None, + start_timestamp: typing.Optional[dt.datetime] = None, + end_timestamp: typing.Optional[dt.datetime] = None, + agent_id: typing.Optional[str] = None, + created_by_me: typing.Optional[bool] = None, + metadata: typing.Optional[SessionMetadata] = None, + source_type: typing.Optional[SessionSourceType] = None, + source_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListSessionsResponse]: + """ + List the sessions (newest first by default). + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + order : typing.Optional[ListSessionsOrder] + Sort sessions by `updated_at`. Defaults to "desc". + + page_token : typing.Optional[str] + Opaque keyset cursor from a previous response `next_page_token`. + + start_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on `created_at` (ISO-8601 / RFC 3339). + + end_timestamp : typing.Optional[dt.datetime] + Inclusive upper bound on `created_at` (ISO-8601 / RFC 3339). + + agent_id : typing.Optional[str] + When set, only sessions bound to this agent id are returned. + + created_by_me : typing.Optional[bool] + When true, only sessions created by the authenticated subject. + + metadata : typing.Optional[SessionMetadata] + Exact metadata pairs as metadata[key]=value. Sessions must contain all pairs. + + source_type : typing.Optional[SessionSourceType] + When set, returns only sessions created by this source type. + + source_id : typing.Optional[str] + When set, returns only sessions from this specific source. Requires source_type. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListSessionsResponse] + Paginated sessions. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/sessions", + method="GET", + params={ + "limit": limit, + "order": order, + "page_token": page_token, + "start_timestamp": serialize_datetime(start_timestamp) if start_timestamp is not None else None, + "end_timestamp": serialize_datetime(end_timestamp) if end_timestamp is not None else None, + "agent_id": agent_id, + "created_by_me": created_by_me, + "metadata": metadata, + "source_type": source_type, + "source_id": source_id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSessionsResponse, + construct_type( + type_=ListSessionsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, + *, + agent: CreateSessionAgent, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSessionResponse]: + """ + Create a session with `agent` as either `{ name }` (named registry binding) or `{ spec: AgentSpec }` (inline). Named sessions snapshot the agent name at create and resolve the live agent on each turn. Responses use `{ type: "reference", name, id }` or `{ type: "inline", spec }`. + + Parameters + ---------- + agent : CreateSessionAgent + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionResponse] + Session created. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/sessions", + method="POST", + json={ + "agent": convert_and_respect_annotation_metadata( + object_=agent, annotation=CreateSessionAgent, direction="write" + ), + "metadata": metadata, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSessionResponse]: + """ + Fetch a session by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionResponse] + Session data. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Delete a session and all related turns, events, and internal state. Only the session creator may delete it. Idempotent if already gone. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + *, + session_id: str, + agent: typing.Optional[SessionAgentSpecBody] = OMIT, + metadata: typing.Optional[SessionMetadata] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSessionResponse]: + """ + Update a session by replacing `agent` with `{ spec: AgentSpec }`. Named (reference) sessions reject agent updates. An empty body is a valid no-op that refreshes `updated_at`. Only the session creator may update it. + + Parameters + ---------- + session_id : str + Session identifier. + + agent : typing.Optional[SessionAgentSpecBody] + + metadata : typing.Optional[SessionMetadata] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSessionResponse] + Session updated. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}", + method="PATCH", + json={ + "agent": convert_and_respect_annotation_metadata( + object_=agent, annotation=SessionAgentSpecBody, direction="write" + ), + "metadata": metadata, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSessionResponse, + construct_type( + type_=GetSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def cancel( + self, *, session_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[CancelSessionResponse]: + """ + Cancel the running last turn for a session. Only the session creator may cancel. + + Parameters + ---------- + session_id : str + Session identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CancelSessionResponse] + Turn cancelled. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/cancel", + method="POST", + json={}, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CancelSessionResponse, + construct_type( + type_=CancelSessionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list_events( + self, + *, + session_id: str, + page_token: typing.Optional[str] = None, + last_turn_id: typing.Optional[str] = None, + limit: typing.Optional[int] = 100, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListSessionEventsResponse]: + """ + List session events as `{ turn_id, event }` across the active turn branch (newest first), including persisted events from a running tip. Each turn contributes turn.created, content events (model.message, tool.call, …), and turn.done when terminal; streaming deltas are not included. Use `page_token` to paginate backward toward older events while retaining the original branch anchor. Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + page_token : typing.Optional[str] + Pagination cursor from `pagination.next_page_token`. It retains the branch anchor turn and returns older events toward the session start. + + last_turn_id : typing.Optional[str] + Newest turn in the listing window (initial load only; ignored when `page_token` is set). Lists that turn and its ancestors, newest events first. Omit to use the session last turn. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListSessionEventsResponse] + Paginated session events. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/events", + method="GET", + params={ + "page_token": page_token, + "last_turn_id": last_turn_id, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSessionEventsResponse, + construct_type( + type_=ListSessionEventsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list_turns( + self, + *, + session_id: str, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListTurnsResponse]: + """ + List turns for a session (newest first by default), token-paginated. Only the session creator may list turns. + + Parameters + ---------- + session_id : str + Session identifier. + + limit : typing.Optional[int] + Page size. Defaults to 25, max 25. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListTurnsResponse] + Paginated turns. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListTurnsResponse, + construct_type( + type_=ListTurnsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.asynccontextmanager + async def create_turn_stream( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[AsyncHttpResponse[AsyncStream[TurnStreamingEvent]]]: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[AsyncHttpResponse[AsyncStream[TurnStreamingEvent]]] + + """ + async with self._client_wrapper.httpx_client.stream( + f"api/v1/sessions/{encode_path_param(session_id)}/turns", + method="POST", + json={ + "input": convert_and_respect_annotation_metadata( + object_=input, annotation=typing.Sequence[TurnInputItem], direction="write" + ), + "previous_turn_id": convert_and_respect_annotation_metadata( + object_=previous_turn_id, annotation=PreviousTurnIdInput, direction="write" + ), + "stream": True, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + async def _stream() -> AsyncHttpResponse[AsyncStream[TurnStreamingEvent]]: + try: + if 200 <= _response.status_code < 300: + + async def _iter() -> typing.AsyncGenerator[StreamEvent[TurnStreamingEvent], None]: + _event_source = EventSource(_response) + async for _sse in _event_source.aiter_sse(): + if _sse.data == None: + return + if len(_sse.data) == 0: + continue + try: + yield StreamEvent( + data=typing.cast( + TurnStreamingEvent, + parse_sse_obj( + sse=_sse, + type_=TurnStreamingEvent, # type: ignore + ), + ), + id=_sse.id or None, + event=_sse.event, + retry=_sse.retry, + ) + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + return + + return AsyncHttpResponse(response=_response, data=AsyncStream(events=_iter)) + await _response.aread() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() + + async def create_turn( + self, + *, + session_id: str, + input: typing.Optional[typing.Sequence[TurnInputItem]] = OMIT, + previous_turn_id: typing.Optional[PreviousTurnIdInput] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetTurnResponse]: + """ + Create a turn within a session and execute it. + Only the session creator may create turns. + When `stream` is true (default), respond with a Server-Sent Events stream of turn events. + When `stream` is false, return the turn immediately with `state.status: "running"` while execution continues in the background; use get turn or subscribe to observe completion. + Use `previous_turn_id` to chain to the session's last turn (defaults to `auto`); use `none` for a new root. + + Parameters + ---------- + session_id : str + Session identifier. + + input : typing.Optional[typing.Sequence[TurnInputItem]] + Turn input items: user messages and/or approval/tool-response resumes. Do not mix user messages with approval or tool-response items. + + previous_turn_id : typing.Optional[PreviousTurnIdInput] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetTurnResponse] + + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns", + method="POST", + json={ + "input": convert_and_respect_annotation_metadata( + object_=input, annotation=typing.Sequence[TurnInputItem], direction="write" + ), + "previous_turn_id": convert_and_respect_annotation_metadata( + object_=previous_turn_id, annotation=PreviousTurnIdInput, direction="write" + ), + "stream": False, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetTurnResponse, + construct_type( + type_=GetTurnResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_turn( + self, *, session_id: str, turn_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetTurnResponse]: + """ + Fetch a single turn by ID. Only the session creator may fetch it. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetTurnResponse] + Turn data. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetTurnResponse, + construct_type( + type_=GetTurnResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.asynccontextmanager + async def download_sandbox_file( + self, *, session_id: str, turn_id: str, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: + """ + Download a file from the sandbox this turn ran in. Paths come from the assistant's `sandbox_artifacts` block. Only the session creator may download. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + path : str + Absolute or sandbox-working-directory-relative file path, as listed in the assistant's `sandbox_artifacts` block. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]] + File contents. + """ + async with self._client_wrapper.httpx_client.stream( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/download-sandbox-file", + method="GET", + params={ + "path": path, + }, + request_options=request_options, + ) as _response: + + async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return AsyncHttpResponse( + response=_response, + data=(_chunk async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size)), + ) + await _response.aread() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 410: + raise GoneError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() + + async def list_turn_events( + self, + *, + session_id: str, + turn_id: str, + limit: typing.Optional[int] = 100, + page_token: typing.Optional[str] = None, + order: typing.Optional[ListTurnEventsOrder] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListTurnEventsResponse]: + """ + Paginated persisted events for a turn (insertion order by default). Only the session creator may list events. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + limit : typing.Optional[int] + Page size. Defaults to 100, max 100. + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + order : typing.Optional[ListTurnEventsOrder] + Sort events by insertion order. Defaults to "asc". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListTurnEventsResponse] + Paginated turn events. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + "order": order, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListTurnEventsResponse, + construct_type( + type_=ListTurnEventsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.asynccontextmanager + async def subscribe_to_turn( + self, + *, + session_id: str, + turn_id: str, + after_sequence_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[AsyncHttpResponse[AsyncStream[TurnStreamingEvent]]]: + """ + Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + after_sequence_number : typing.Optional[int] + Exclusive resume cursor: replay only events with a sequence number greater than this value. Omit to start from the beginning of the live buffer. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[AsyncHttpResponse[AsyncStream[TurnStreamingEvent]]] + Server-Sent Events stream of turn events (deltas and lifecycle). + """ + async with self._client_wrapper.httpx_client.stream( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/subscribe", + method="GET", + params={ + "after_sequence_number": after_sequence_number, + }, + request_options=request_options, + ) as _response: + + async def _stream() -> AsyncHttpResponse[AsyncStream[TurnStreamingEvent]]: + try: + if 200 <= _response.status_code < 300: + + async def _iter() -> typing.AsyncGenerator[StreamEvent[TurnStreamingEvent], None]: + _event_source = EventSource(_response) + async for _sse in _event_source.aiter_sse(): + if _sse.data == None: + return + if len(_sse.data) == 0: + continue + try: + yield StreamEvent( + data=typing.cast( + TurnStreamingEvent, + parse_sse_obj( + sse=_sse, + type_=TurnStreamingEvent, # type: ignore + ), + ), + id=_sse.id or None, + event=_sse.event, + retry=_sse.retry, + ) + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + return + + return AsyncHttpResponse(response=_response, data=AsyncStream(events=_iter)) + await _response.aread() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 412: + raise PreconditionFailedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py new file mode 100644 index 000000000..c8555edfe --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import mcp_servers, model_providers, sandbox_providers, skills +_dynamic_imports: typing.Dict[str, str] = { + "mcp_servers": ".mcp_servers", + "model_providers": ".model_providers", + "sandbox_providers": ".sandbox_providers", + "skills": ".skills", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["mcp_servers", "model_providers", "sandbox_providers", "skills"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/client.py new file mode 100644 index 000000000..26014cb68 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/client.py @@ -0,0 +1,120 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .raw_client import AsyncRawSettingsClient, RawSettingsClient + +if typing.TYPE_CHECKING: + from .mcp_servers.client import AsyncMcpServersClient, McpServersClient + from .model_providers.client import AsyncModelProvidersClient, ModelProvidersClient + from .sandbox_providers.client import AsyncSandboxProvidersClient, SandboxProvidersClient + from .skills.client import AsyncSkillsClient, SkillsClient + + +class SettingsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSettingsClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._mcp_servers: typing.Optional[McpServersClient] = None + self._model_providers: typing.Optional[ModelProvidersClient] = None + self._sandbox_providers: typing.Optional[SandboxProvidersClient] = None + self._skills: typing.Optional[SkillsClient] = None + + @property + def with_raw_response(self) -> RawSettingsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSettingsClient + """ + return self._raw_client + + @property + def mcp_servers(self): + if self._mcp_servers is None: + from .mcp_servers.client import McpServersClient # noqa: E402 + + self._mcp_servers = McpServersClient(client_wrapper=self._client_wrapper) + return self._mcp_servers + + @property + def model_providers(self): + if self._model_providers is None: + from .model_providers.client import ModelProvidersClient # noqa: E402 + + self._model_providers = ModelProvidersClient(client_wrapper=self._client_wrapper) + return self._model_providers + + @property + def sandbox_providers(self): + if self._sandbox_providers is None: + from .sandbox_providers.client import SandboxProvidersClient # noqa: E402 + + self._sandbox_providers = SandboxProvidersClient(client_wrapper=self._client_wrapper) + return self._sandbox_providers + + @property + def skills(self): + if self._skills is None: + from .skills.client import SkillsClient # noqa: E402 + + self._skills = SkillsClient(client_wrapper=self._client_wrapper) + return self._skills + + +class AsyncSettingsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSettingsClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._mcp_servers: typing.Optional[AsyncMcpServersClient] = None + self._model_providers: typing.Optional[AsyncModelProvidersClient] = None + self._sandbox_providers: typing.Optional[AsyncSandboxProvidersClient] = None + self._skills: typing.Optional[AsyncSkillsClient] = None + + @property + def with_raw_response(self) -> AsyncRawSettingsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSettingsClient + """ + return self._raw_client + + @property + def mcp_servers(self): + if self._mcp_servers is None: + from .mcp_servers.client import AsyncMcpServersClient # noqa: E402 + + self._mcp_servers = AsyncMcpServersClient(client_wrapper=self._client_wrapper) + return self._mcp_servers + + @property + def model_providers(self): + if self._model_providers is None: + from .model_providers.client import AsyncModelProvidersClient # noqa: E402 + + self._model_providers = AsyncModelProvidersClient(client_wrapper=self._client_wrapper) + return self._model_providers + + @property + def sandbox_providers(self): + if self._sandbox_providers is None: + from .sandbox_providers.client import AsyncSandboxProvidersClient # noqa: E402 + + self._sandbox_providers = AsyncSandboxProvidersClient(client_wrapper=self._client_wrapper) + return self._sandbox_providers + + @property + def skills(self): + if self._skills is None: + from .skills.client import AsyncSkillsClient # noqa: E402 + + self._skills = AsyncSkillsClient(client_wrapper=self._client_wrapper) + return self._skills diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/client.py new file mode 100644 index 000000000..4f362251d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/client.py @@ -0,0 +1,343 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_mcp_server_response import GetMcpServerResponse +from ...types.list_mcp_servers_response import ListMcpServersResponse +from ...types.mcp_server_manifest import McpServerManifest +from .raw_client import AsyncRawMcpServersClient, RawMcpServersClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class McpServersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMcpServersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMcpServersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMcpServersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListMcpServersResponse: + """ + Configured MCP servers with auth_status. Header secrets are redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListMcpServersResponse + All configured MCP servers + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.mcp_servers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def create( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetMcpServerResponse: + """ + Creates an MCP server by `name`. Fails if `name` is already taken. Runs DCR registration when `auth.type` is `dcr`. Header secrets: real value required; redacted with no stored value returns 400. + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The created MCP server with auth_status + + Examples + -------- + from trueforge_sdk import RemoteMcpServerManifest, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.mcp_servers.create( + manifest=RemoteMcpServerManifest( + description="description", + name="name", + url="url", + ), + ) + """ + _response = self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + def create_or_update( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetMcpServerResponse: + """ + Create or replace by `name`. Header secrets: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The saved MCP server with auth_status + + Examples + -------- + from trueforge_sdk import RemoteMcpServerManifest, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.mcp_servers.create_or_update( + manifest=RemoteMcpServerManifest( + description="description", + name="name", + url="url", + ), + ) + """ + _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data + + def get(self, *, name: str, request_options: typing.Optional[RequestOptions] = None) -> GetMcpServerResponse: + """ + A single MCP server by name, with nested live auth_status (settings / admin projection). Header auth values are redacted. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The MCP server + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.mcp_servers.get( + name="name", + ) + """ + _response = self._raw_client.get(name=name, request_options=request_options) + return _response.data + + +class AsyncMcpServersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMcpServersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMcpServersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMcpServersClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListMcpServersResponse: + """ + Configured MCP servers with auth_status. Header secrets are redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListMcpServersResponse + All configured MCP servers + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.mcp_servers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def create( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetMcpServerResponse: + """ + Creates an MCP server by `name`. Fails if `name` is already taken. Runs DCR registration when `auth.type` is `dcr`. Header secrets: real value required; redacted with no stored value returns 400. + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The created MCP server with auth_status + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, RemoteMcpServerManifest + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.mcp_servers.create( + manifest=RemoteMcpServerManifest( + description="description", + name="name", + url="url", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + async def create_or_update( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetMcpServerResponse: + """ + Create or replace by `name`. Header secrets: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The saved MCP server with auth_status + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, RemoteMcpServerManifest + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.mcp_servers.create_or_update( + manifest=RemoteMcpServerManifest( + description="description", + name="name", + url="url", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data + + async def get(self, *, name: str, request_options: typing.Optional[RequestOptions] = None) -> GetMcpServerResponse: + """ + A single MCP server by name, with nested live auth_status (settings / admin projection). Header auth values are redacted. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMcpServerResponse + The MCP server + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.mcp_servers.get( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(name=name, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/raw_client.py new file mode 100644 index 000000000..7debcbada --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/mcp_servers/raw_client.py @@ -0,0 +1,626 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import encode_path_param +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...errors.conflict_error import ConflictError +from ...errors.failed_dependency_error import FailedDependencyError +from ...errors.forbidden_error import ForbiddenError +from ...errors.not_found_error import NotFoundError +from ...errors.unauthorized_error import UnauthorizedError +from ...errors.unprocessable_entity_error import UnprocessableEntityError +from ...types.get_mcp_server_response import GetMcpServerResponse +from ...types.list_mcp_servers_response import ListMcpServersResponse +from ...types.mcp_server_manifest import McpServerManifest +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawMcpServersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[ListMcpServersResponse]: + """ + Configured MCP servers with auth_status. Header secrets are redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListMcpServersResponse] + All configured MCP servers + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/mcp-servers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListMcpServersResponse, + construct_type( + type_=ListMcpServersResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetMcpServerResponse]: + """ + Creates an MCP server by `name`. Fails if `name` is already taken. Runs DCR registration when `auth.type` is `dcr`. Header secrets: real value required; redacted with no stored value returns 400. + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMcpServerResponse] + The created MCP server with auth_status + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/mcp-servers", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=McpServerManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_or_update( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetMcpServerResponse]: + """ + Create or replace by `name`. Header secrets: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMcpServerResponse] + The saved MCP server with auth_status + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/mcp-servers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=McpServerManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetMcpServerResponse]: + """ + A single MCP server by name, with nested live auth_status (settings / admin projection). Header auth values are redacted. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMcpServerResponse] + The MCP server + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/settings/mcp-servers/{encode_path_param(name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawMcpServersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListMcpServersResponse]: + """ + Configured MCP servers with auth_status. Header secrets are redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListMcpServersResponse] + All configured MCP servers + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/mcp-servers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListMcpServersResponse, + construct_type( + type_=ListMcpServersResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetMcpServerResponse]: + """ + Creates an MCP server by `name`. Fails if `name` is already taken. Runs DCR registration when `auth.type` is `dcr`. Header secrets: real value required; redacted with no stored value returns 400. + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMcpServerResponse] + The created MCP server with auth_status + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/mcp-servers", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=McpServerManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_or_update( + self, *, manifest: McpServerManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetMcpServerResponse]: + """ + Create or replace by `name`. Header secrets: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : McpServerManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMcpServerResponse] + The saved MCP server with auth_status + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/mcp-servers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=McpServerManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetMcpServerResponse]: + """ + A single MCP server by name, with nested live auth_status (settings / admin projection). Header auth values are redacted. + + Parameters + ---------- + name : str + MCP server name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMcpServerResponse] + The MCP server + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/settings/mcp-servers/{encode_path_param(name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMcpServerResponse, + construct_type( + type_=GetMcpServerResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/client.py new file mode 100644 index 000000000..9a39bca0e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/client.py @@ -0,0 +1,323 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_model_provider_response import GetModelProviderResponse +from ...types.list_model_providers_response import ListModelProvidersResponse +from ...types.model_provider_manifest import ModelProviderManifest +from .raw_client import AsyncRawModelProvidersClient, RawModelProvidersClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ModelProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawModelProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawModelProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawModelProvidersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListModelProvidersResponse: + """ + All configured providers with nested manifests. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListModelProvidersResponse + All configured model providers + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.model_providers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def create( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetModelProviderResponse: + """ + Creates a provider (models included). Fails if `name` is already taken. Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetModelProviderResponse + The created provider + + Examples + -------- + from trueforge_sdk import ( + AlibabaModelProvider, + ConfiguredModel, + ModelProperties, + ModelProviderAuth, + TrueForge, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.model_providers.create( + manifest=AlibabaModelProvider( + auth=ModelProviderAuth( + api_key="api_key", + ), + models=[ + ConfiguredModel( + model_id="model_id", + name="name", + properties=ModelProperties(), + ) + ], + ), + ) + """ + _response = self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + def create_or_update( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetModelProviderResponse: + """ + Create or replace a provider (models included). Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetModelProviderResponse + The saved provider + + Examples + -------- + from trueforge_sdk import ( + AlibabaModelProvider, + ConfiguredModel, + ModelProperties, + ModelProviderAuth, + TrueForge, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.model_providers.create_or_update( + manifest=AlibabaModelProvider( + auth=ModelProviderAuth( + api_key="api_key", + ), + models=[ + ConfiguredModel( + model_id="model_id", + name="name", + properties=ModelProperties(), + ) + ], + ), + ) + """ + _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data + + +class AsyncModelProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawModelProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawModelProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawModelProvidersClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListModelProvidersResponse: + """ + All configured providers with nested manifests. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListModelProvidersResponse + All configured model providers + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.model_providers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def create( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetModelProviderResponse: + """ + Creates a provider (models included). Fails if `name` is already taken. Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetModelProviderResponse + The created provider + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AlibabaModelProvider, + AsyncTrueForge, + ConfiguredModel, + ModelProperties, + ModelProviderAuth, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.model_providers.create( + manifest=AlibabaModelProvider( + auth=ModelProviderAuth( + api_key="api_key", + ), + models=[ + ConfiguredModel( + model_id="model_id", + name="name", + properties=ModelProperties(), + ) + ], + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + async def create_or_update( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetModelProviderResponse: + """ + Create or replace a provider (models included). Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetModelProviderResponse + The saved provider + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AlibabaModelProvider, + AsyncTrueForge, + ConfiguredModel, + ModelProperties, + ModelProviderAuth, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.model_providers.create_or_update( + manifest=AlibabaModelProvider( + auth=ModelProviderAuth( + api_key="api_key", + ), + models=[ + ConfiguredModel( + model_id="model_id", + name="name", + properties=ModelProperties(), + ) + ], + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/raw_client.py new file mode 100644 index 000000000..a05073e05 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/model_providers/raw_client.py @@ -0,0 +1,473 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...errors.conflict_error import ConflictError +from ...errors.failed_dependency_error import FailedDependencyError +from ...errors.forbidden_error import ForbiddenError +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_model_provider_response import GetModelProviderResponse +from ...types.list_model_providers_response import ListModelProvidersResponse +from ...types.model_provider_manifest import ModelProviderManifest +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawModelProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListModelProvidersResponse]: + """ + All configured providers with nested manifests. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListModelProvidersResponse] + All configured model providers + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/model-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListModelProvidersResponse, + construct_type( + type_=ListModelProvidersResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetModelProviderResponse]: + """ + Creates a provider (models included). Fails if `name` is already taken. Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetModelProviderResponse] + The created provider + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/model-providers", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ModelProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetModelProviderResponse, + construct_type( + type_=GetModelProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_or_update( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetModelProviderResponse]: + """ + Create or replace a provider (models included). Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetModelProviderResponse] + The saved provider + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/model-providers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ModelProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetModelProviderResponse, + construct_type( + type_=GetModelProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawModelProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListModelProvidersResponse]: + """ + All configured providers with nested manifests. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListModelProvidersResponse] + All configured model providers + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/model-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListModelProvidersResponse, + construct_type( + type_=ListModelProvidersResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetModelProviderResponse]: + """ + Creates a provider (models included). Fails if `name` is already taken. Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetModelProviderResponse] + The created provider + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/model-providers", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ModelProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetModelProviderResponse, + construct_type( + type_=GetModelProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_or_update( + self, *, manifest: ModelProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetModelProviderResponse]: + """ + Create or replace a provider (models included). Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : ModelProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetModelProviderResponse] + The saved provider + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/model-providers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=ModelProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetModelProviderResponse, + construct_type( + type_=GetModelProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/raw_client.py new file mode 100644 index 000000000..d02cb822b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/raw_client.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper + + +class RawSettingsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + +class AsyncRawSettingsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/client.py new file mode 100644 index 000000000..cbf634239 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/client.py @@ -0,0 +1,204 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_sandbox_provider_response import GetSandboxProviderResponse +from ...types.sandbox_provider_manifest import SandboxProviderManifest +from .raw_client import AsyncRawSandboxProvidersClient, RawSandboxProvidersClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SandboxProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSandboxProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSandboxProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSandboxProvidersClient + """ + return self._raw_client + + def get(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetSandboxProviderResponse: + """ + The single configured sandbox provider for this tenant. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxProviderResponse + The configured sandbox provider. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.sandbox_providers.get() + """ + _response = self._raw_client.get(request_options=request_options) + return _response.data + + def create_or_update( + self, *, manifest: SandboxProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetSandboxProviderResponse: + """ + Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : SandboxProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxProviderResponse + The saved sandbox provider. + + Examples + -------- + from trueforge_sdk import ( + DaytonaSandboxProviderAuth, + SandboxProviderManifest, + TrueForge, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.sandbox_providers.create_or_update( + manifest=SandboxProviderManifest( + auth=DaytonaSandboxProviderAuth( + api_key="api_key", + ), + auto_archive_interval_in_minutes=1, + auto_delete_interval_in_minutes=1, + auto_stop_interval_in_minutes=1, + exec_timeout_ms=1, + ), + ) + """ + _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data + + +class AsyncSandboxProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSandboxProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSandboxProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSandboxProvidersClient + """ + return self._raw_client + + async def get(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetSandboxProviderResponse: + """ + The single configured sandbox provider for this tenant. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxProviderResponse + The configured sandbox provider. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.sandbox_providers.get() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(request_options=request_options) + return _response.data + + async def create_or_update( + self, *, manifest: SandboxProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetSandboxProviderResponse: + """ + Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : SandboxProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxProviderResponse + The saved sandbox provider. + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AsyncTrueForge, + DaytonaSandboxProviderAuth, + SandboxProviderManifest, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.sandbox_providers.create_or_update( + manifest=SandboxProviderManifest( + auth=DaytonaSandboxProviderAuth( + api_key="api_key", + ), + auto_archive_interval_in_minutes=1, + auto_delete_interval_in_minutes=1, + auto_stop_interval_in_minutes=1, + exec_timeout_ms=1, + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/raw_client.py new file mode 100644 index 000000000..022842fdc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/sandbox_providers/raw_client.py @@ -0,0 +1,280 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...errors.not_found_error import NotFoundError +from ...errors.unprocessable_entity_error import UnprocessableEntityError +from ...types.get_sandbox_provider_response import GetSandboxProviderResponse +from ...types.request_error_response import RequestErrorResponse +from ...types.sandbox_provider_manifest import SandboxProviderManifest +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSandboxProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSandboxProviderResponse]: + """ + The single configured sandbox provider for this tenant. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSandboxProviderResponse] + The configured sandbox provider. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/sandbox-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxProviderResponse, + construct_type( + type_=GetSandboxProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_or_update( + self, *, manifest: SandboxProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSandboxProviderResponse]: + """ + Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : SandboxProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSandboxProviderResponse] + The saved sandbox provider. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/sandbox-providers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SandboxProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxProviderResponse, + construct_type( + type_=GetSandboxProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSandboxProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSandboxProviderResponse]: + """ + The single configured sandbox provider for this tenant. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSandboxProviderResponse] + The configured sandbox provider. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/sandbox-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxProviderResponse, + construct_type( + type_=GetSandboxProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_or_update( + self, *, manifest: SandboxProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSandboxProviderResponse]: + """ + Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : SandboxProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSandboxProviderResponse] + The saved sandbox provider. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/sandbox-providers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SandboxProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxProviderResponse, + construct_type( + type_=GetSandboxProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/skills/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/skills/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/skills/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/skills/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/skills/client.py new file mode 100644 index 000000000..c5a7a19c8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/skills/client.py @@ -0,0 +1,275 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_skill_response import GetSkillResponse +from ...types.list_skills_response import ListSkillsResponse +from ...types.skill_manifest import SkillManifest +from .raw_client import AsyncRawSkillsClient, RawSkillsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSkillsClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListSkillsResponse: + """ + All configured skills with nested manifests (settings / admin projection). + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSkillsResponse + All configured skills. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.skills.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def create( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetSkillResponse: + """ + Creates a skill keyed by `name`. Fails if `name` is already taken. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSkillResponse + The created skill. + + Examples + -------- + from trueforge_sdk import GitSkill, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.skills.create( + manifest=GitSkill( + description="description", + name="name", + ref="ref", + url="url", + ), + ) + """ + _response = self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + def create_or_update( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetSkillResponse: + """ + Full upsert keyed by `name`: creates the skill or replaces its entire manifest. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSkillResponse + The saved skill. + + Examples + -------- + from trueforge_sdk import GitSkill, TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.skills.create_or_update( + manifest=GitSkill( + description="description", + name="name", + ref="ref", + url="url", + ), + ) + """ + _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data + + +class AsyncSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSkillsClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListSkillsResponse: + """ + All configured skills with nested manifests (settings / admin projection). + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSkillsResponse + All configured skills. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.skills.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def create( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetSkillResponse: + """ + Creates a skill keyed by `name`. Fails if `name` is already taken. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSkillResponse + The created skill. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, GitSkill + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.skills.create( + manifest=GitSkill( + description="description", + name="name", + ref="ref", + url="url", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + async def create_or_update( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetSkillResponse: + """ + Full upsert keyed by `name`: creates the skill or replaces its entire manifest. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSkillResponse + The saved skill. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge, GitSkill + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.skills.create_or_update( + manifest=GitSkill( + description="description", + name="name", + ref="ref", + url="url", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/skills/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/skills/raw_client.py new file mode 100644 index 000000000..beac8859c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/skills/raw_client.py @@ -0,0 +1,471 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...errors.conflict_error import ConflictError +from ...errors.failed_dependency_error import FailedDependencyError +from ...errors.forbidden_error import ForbiddenError +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_skill_response import GetSkillResponse +from ...types.list_skills_response import ListSkillsResponse +from ...types.request_error_response import RequestErrorResponse +from ...types.skill_manifest import SkillManifest +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[ListSkillsResponse]: + """ + All configured skills with nested manifests (settings / admin projection). + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListSkillsResponse] + All configured skills. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/skills", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSkillsResponse, + construct_type( + type_=ListSkillsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSkillResponse]: + """ + Creates a skill keyed by `name`. Fails if `name` is already taken. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSkillResponse] + The created skill. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/skills", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SkillManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSkillResponse, + construct_type( + type_=GetSkillResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_or_update( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSkillResponse]: + """ + Full upsert keyed by `name`: creates the skill or replaces its entire manifest. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSkillResponse] + The saved skill. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/skills", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SkillManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSkillResponse, + construct_type( + type_=GetSkillResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListSkillsResponse]: + """ + All configured skills with nested manifests (settings / admin projection). + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListSkillsResponse] + All configured skills. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/skills", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSkillsResponse, + construct_type( + type_=ListSkillsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSkillResponse]: + """ + Creates a skill keyed by `name`. Fails if `name` is already taken. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSkillResponse] + The created skill. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/skills", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SkillManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSkillResponse, + construct_type( + type_=GetSkillResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_or_update( + self, *, manifest: SkillManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSkillResponse]: + """ + Full upsert keyed by `name`: creates the skill or replaces its entire manifest. + + Parameters + ---------- + manifest : SkillManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSkillResponse] + The saved skill. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/skills", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SkillManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSkillResponse, + construct_type( + type_=GetSkillResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/skills/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/skills/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/skills/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/skills/client.py b/python/trueforge_sdk/src/trueforge_sdk/skills/client.py new file mode 100644 index 000000000..e98889a7f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/skills/client.py @@ -0,0 +1,179 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.list_available_skills_response import ListAvailableSkillsResponse +from ..types.list_skill_versions_response import ListSkillVersionsResponse +from .raw_client import AsyncRawSkillsClient, RawSkillsClient + + +class SkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSkillsClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAvailableSkillsResponse: + """ + Configured skills as a slim name/description list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAvailableSkillsResponse + All configured skills (chat projection). + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.skills.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def list_versions( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> ListSkillVersionsResponse: + """ + Versions for one skill. + + Parameters + ---------- + name : str + Skill name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSkillVersionsResponse + Skill versions. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.skills.list_versions( + name="name", + ) + """ + _response = self._raw_client.list_versions(name=name, request_options=request_options) + return _response.data + + +class AsyncSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSkillsClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAvailableSkillsResponse: + """ + Configured skills as a slim name/description list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListAvailableSkillsResponse + All configured skills (chat projection). + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.skills.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def list_versions( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> ListSkillVersionsResponse: + """ + Versions for one skill. + + Parameters + ---------- + name : str + Skill name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListSkillVersionsResponse + Skill versions. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.skills.list_versions( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_versions(name=name, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/skills/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/skills/raw_client.py new file mode 100644 index 000000000..f15c75bfc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/skills/raw_client.py @@ -0,0 +1,242 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.unauthorized_error import UnauthorizedError +from ..types.list_available_skills_response import ListAvailableSkillsResponse +from ..types.list_skill_versions_response import ListSkillVersionsResponse +from ..types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawSkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListAvailableSkillsResponse]: + """ + Configured skills as a slim name/description list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListAvailableSkillsResponse] + All configured skills (chat projection). + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/skills", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAvailableSkillsResponse, + construct_type( + type_=ListAvailableSkillsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list_versions( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListSkillVersionsResponse]: + """ + Versions for one skill. + + Parameters + ---------- + name : str + Skill name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListSkillVersionsResponse] + Skill versions. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/skills/versions", + method="GET", + params={ + "name": name, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSkillVersionsResponse, + construct_type( + type_=ListSkillVersionsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListAvailableSkillsResponse]: + """ + Configured skills as a slim name/description list for the composer. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListAvailableSkillsResponse] + All configured skills (chat projection). + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/skills", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListAvailableSkillsResponse, + construct_type( + type_=ListAvailableSkillsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list_versions( + self, *, name: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListSkillVersionsResponse]: + """ + Versions for one skill. + + Parameters + ---------- + name : str + Skill name. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListSkillVersionsResponse] + Skill versions. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/skills/versions", + method="GET", + params={ + "name": name, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListSkillVersionsResponse, + construct_type( + type_=ListSkillVersionsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py new file mode 100644 index 000000000..a77f06cbc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -0,0 +1,722 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .action_required_event import ActionRequiredEvent + from .agent import Agent + from .agent_code_snippet import AgentCodeSnippet + from .agent_code_snippet_sample_code import AgentCodeSnippetSampleCode + from .agent_code_snippets import AgentCodeSnippets + from .agent_info import AgentInfo + from .agent_parent import AgentParent + from .agent_spec import AgentSpec + from .alibaba_model_provider import AlibabaModelProvider + from .anthropic_model_provider import AnthropicModelProvider + from .approval_allow import ApprovalAllow + from .approval_decision import ApprovalDecision + from .approval_deny import ApprovalDeny + from .ask_user_questions_config import AskUserQuestionsConfig + from .available_mcp_server import AvailableMcpServer + from .available_model import AvailableModel + from .available_model_provider import AvailableModelProvider + from .available_skill import AvailableSkill + from .base_mcp_auth_required_event import BaseMcpAuthRequiredEvent + from .base_thread_done_event import BaseThreadDoneEvent + from .cancel_session_response import CancelSessionResponse + from .capabilities_data import CapabilitiesData + from .catalog_custom_model_provider import CatalogCustomModelProvider + from .catalog_mcp_server import CatalogMcpServer + from .catalog_mcp_server_type import CatalogMcpServerType + from .catalog_model import CatalogModel + from .catalog_model_provider import CatalogModelProvider + from .catalog_sandbox_provider import CatalogSandboxProvider + from .catalog_skill import CatalogSkill + from .catalog_skill_type import CatalogSkillType + from .catalog_well_known_model_provider import CatalogWellKnownModelProvider + from .catalog_well_known_model_provider_type import CatalogWellKnownModelProviderType + from .chat_completion_chunk_delta_tool_call import ChatCompletionChunkDeltaToolCall + from .chat_completion_chunk_delta_tool_call_function import ChatCompletionChunkDeltaToolCallFunction + from .chat_completion_content_part_refusal import ChatCompletionContentPartRefusal + from .chat_completion_content_part_text import ChatCompletionContentPartText + from .chat_completion_message_tool_call import ChatCompletionMessageToolCall + from .chat_completion_message_tool_call_function import ChatCompletionMessageToolCallFunction + from .compaction_config import CompactionConfig + from .configured_mcp_server import ConfiguredMcpServer + from .configured_model import ConfiguredModel + from .configured_model_provider import ConfiguredModelProvider + from .configured_sandbox_provider import ConfiguredSandboxProvider + from .configured_skill import ConfiguredSkill + from .context_management_config import ContextManagementConfig + from .create_schedule_run_response import CreateScheduleRunResponse + from .create_session_agent import CreateSessionAgent + from .created_by_subject import CreatedBySubject + from .cron_expression import CronExpression + from .custom_model_provider import CustomModelProvider + from .daytona_sandbox_provider_auth import DaytonaSandboxProviderAuth + from .delete_agent_response import DeleteAgentResponse + from .delete_schedule_response import DeleteScheduleResponse + from .dynamic_sub_agents_config import DynamicSubAgentsConfig + from .extended_chunk_delta_tool_call import ExtendedChunkDeltaToolCall + from .file_content import FileContent + from .finish_reason import FinishReason + from .fireworks_model_provider import FireworksModelProvider + from .generative_ui_config import GenerativeUiConfig + from .get_agent_code_snippets_response import GetAgentCodeSnippetsResponse + from .get_agent_response import GetAgentResponse + from .get_available_mcp_server_response import GetAvailableMcpServerResponse + from .get_capabilities_response import GetCapabilitiesResponse + from .get_mcp_server_catalog_response import GetMcpServerCatalogResponse + from .get_mcp_server_response import GetMcpServerResponse + from .get_me_response import GetMeResponse + from .get_me_subject import GetMeSubject + from .get_model_provider_catalog_response import GetModelProviderCatalogResponse + from .get_model_provider_response import GetModelProviderResponse + from .get_sandbox_provider_catalog_response import GetSandboxProviderCatalogResponse + from .get_sandbox_provider_response import GetSandboxProviderResponse + from .get_schedule_response import GetScheduleResponse + from .get_session_metrics_chart_data_response import GetSessionMetricsChartDataResponse + from .get_session_metrics_chart_response import GetSessionMetricsChartResponse + from .get_session_metrics_meter_response import GetSessionMetricsMeterResponse + from .get_session_response import GetSessionResponse + from .get_skill_catalog_response import GetSkillCatalogResponse + from .get_skill_response import GetSkillResponse + from .get_turn_response import GetTurnResponse + from .git_skill import GitSkill + from .google_gemini_model_provider import GoogleGeminiModelProvider + from .initial_user_message import InitialUserMessage + from .input_tokens_compaction_trigger import InputTokensCompactionTrigger + from .large_tool_response_config import LargeToolResponseConfig + from .list_agents_response import ListAgentsResponse + from .list_available_mcp_servers_response import ListAvailableMcpServersResponse + from .list_available_models_response import ListAvailableModelsResponse + from .list_available_skills_response import ListAvailableSkillsResponse + from .list_mcp_server_tools_response import ListMcpServerToolsResponse + from .list_mcp_servers_response import ListMcpServersResponse + from .list_model_providers_response import ListModelProvidersResponse + from .list_permissions_response import ListPermissionsResponse + from .list_schedule_runs_response import ListScheduleRunsResponse + from .list_schedules_response import ListSchedulesResponse + from .list_session_events_response import ListSessionEventsResponse + from .list_sessions_order import ListSessionsOrder + from .list_sessions_response import ListSessionsResponse + from .list_skill_versions_response import ListSkillVersionsResponse + from .list_skills_response import ListSkillsResponse + from .list_turn_events_order import ListTurnEventsOrder + from .list_turn_events_response import ListTurnEventsResponse + from .list_turns_response import ListTurnsResponse + from .mcp_auth_required_event import McpAuthRequiredEvent + from .mcp_auth_status import McpAuthStatus + from .mcp_auth_status_status import McpAuthStatusStatus + from .mcp_initialize_event import McpInitializeEvent + from .mcp_server import McpServer + from .mcp_server_approval_tool_selector import McpServerApprovalToolSelector + from .mcp_server_auth_info import McpServerAuthInfo + from .mcp_server_auth_public import McpServerAuthPublic + from .mcp_server_auth_public_one import McpServerAuthPublicOne + from .mcp_server_auth_public_zero import McpServerAuthPublicZero + from .mcp_server_dcr_auth import McpServerDcrAuth + from .mcp_server_header_auth import McpServerHeaderAuth + from .mcp_server_init_info import McpServerInitInfo + from .mcp_server_init_info_transport_type import McpServerInitInfoTransportType + from .mcp_server_manifest import McpServerManifest + from .mcp_server_manifest_auth import McpServerManifestAuth + from .mcp_server_tool_selector import McpServerToolSelector + from .mcp_tool_info import McpToolInfo + from .me import Me + from .me_session_type import MeSessionType + from .metrics_unit import MetricsUnit + from .model import Model + from .model_message_delta_event import ModelMessageDeltaEvent + from .model_message_event import ModelMessageEvent + from .model_message_event_content import ModelMessageEventContent + from .model_message_event_content_one_item import ModelMessageEventContentOneItem + from .model_message_usage import ModelMessageUsage + from .model_message_usage_input_tokens_breakdown import ModelMessageUsageInputTokensBreakdown + from .model_params import ModelParams + from .model_properties import ModelProperties + from .model_provider_auth import ModelProviderAuth + from .model_provider_manifest import ModelProviderManifest + from .moonshot_model_provider import MoonshotModelProvider + from .open_ai_model_provider import OpenAiModelProvider + from .permission_resource_type import PermissionResourceType + from .previous_turn_id_input import PreviousTurnIdInput + from .raw_tool_call import RawToolCall + from .reasoning_effort import ReasoningEffort + from .remote_mcp_server_manifest import RemoteMcpServerManifest + from .request_error_response import RequestErrorResponse + from .request_error_response_error import RequestErrorResponseError + from .resource_name import ResourceName + from .resource_permission import ResourcePermission + from .response_format import ResponseFormat + from .response_format_json_object import ResponseFormatJsonObject + from .response_format_json_schema import ResponseFormatJsonSchema + from .response_format_json_schema_json_schema import ResponseFormatJsonSchemaJsonSchema + from .response_format_text import ResponseFormatText + from .runtime_config import RuntimeConfig + from .sandbox_build_status import SandboxBuildStatus + from .sandbox_capability import SandboxCapability + from .sandbox_config import SandboxConfig + from .sandbox_created_event import SandboxCreatedEvent + from .sandbox_provider_manifest import SandboxProviderManifest + from .schedule import Schedule + from .schedule_manifest import ScheduleManifest + from .schedule_run import ScheduleRun + from .schedule_run_status import ScheduleRunStatus + from .schedule_status import ScheduleStatus + from .session import Session + from .session_agent import SessionAgent + from .session_agent_inline import SessionAgentInline + from .session_agent_name_ref import SessionAgentNameRef + from .session_agent_reference import SessionAgentReference + from .session_agent_spec_body import SessionAgentSpecBody + from .session_event import SessionEvent + from .session_event_item import SessionEventItem + from .session_metadata import SessionMetadata + from .session_metrics import SessionMetrics + from .session_metrics_chart import SessionMetricsChart + from .session_metrics_chart_data_response import SessionMetricsChartDataResponse + from .session_metrics_chart_name import SessionMetricsChartName + from .session_metrics_chart_response import SessionMetricsChartResponse + from .session_metrics_graph import SessionMetricsGraph + from .session_metrics_graph_line import SessionMetricsGraphLine + from .session_metrics_meter import SessionMetricsMeter + from .session_metrics_meter_name import SessionMetricsMeterName + from .session_metrics_meter_response import SessionMetricsMeterResponse + from .session_metrics_point import SessionMetricsPoint + from .session_source import SessionSource + from .session_source_schedule import SessionSourceSchedule + from .session_source_type import SessionSourceType + from .settings_capability import SettingsCapability + from .skill import Skill + from .skill_capability import SkillCapability + from .skill_manifest import SkillManifest + from .skill_version import SkillVersion + from .text_content import TextContent + from .thread_created_event import ThreadCreatedEvent + from .thread_done_event import ThreadDoneEvent + from .thread_state import ThreadState + from .thread_state_done import ThreadStateDone + from .thread_state_error import ThreadStateError + from .timezone import Timezone + from .together_ai_model_provider import TogetherAiModelProvider + from .token_pagination import TokenPagination + from .tool_approval_required_event import ToolApprovalRequiredEvent + from .tool_call import ToolCall + from .tool_call_ref import ToolCallRef + from .tool_info import ToolInfo + from .tool_response_event import ToolResponseEvent + from .tool_response_required_event import ToolResponseRequiredEvent + from .true_foundry_mcp_server_manifest import TrueFoundryMcpServerManifest + from .true_foundry_model_provider import TrueFoundryModelProvider + from .true_foundry_registry_skill import TrueFoundryRegistrySkill + from .true_foundry_system_tool_info import TrueFoundrySystemToolInfo + from .turn import Turn + from .turn_created_event import TurnCreatedEvent + from .turn_done_event import TurnDoneEvent + from .turn_done_event_state import TurnDoneEventState + from .turn_input_item import TurnInputItem + from .turn_metrics import TurnMetrics + from .turn_state import TurnState + from .turn_state_cancelled import TurnStateCancelled + from .turn_state_cancelled_metrics import TurnStateCancelledMetrics + from .turn_state_cancelled_reason import TurnStateCancelledReason + from .turn_state_done import TurnStateDone + from .turn_state_error import TurnStateError + from .turn_state_error_metrics import TurnStateErrorMetrics + from .turn_state_running import TurnStateRunning + from .turn_streaming_event import TurnStreamingEvent + from .user_message import UserMessage + from .user_message_content import UserMessageContent + from .user_message_content_item import UserMessageContentItem + from .user_tool_approval_event import UserToolApprovalEvent + from .user_tool_response_event import UserToolResponseEvent + from .zai_model_provider import ZaiModelProvider +_dynamic_imports: typing.Dict[str, str] = { + "ActionRequiredEvent": ".action_required_event", + "Agent": ".agent", + "AgentCodeSnippet": ".agent_code_snippet", + "AgentCodeSnippetSampleCode": ".agent_code_snippet_sample_code", + "AgentCodeSnippets": ".agent_code_snippets", + "AgentInfo": ".agent_info", + "AgentParent": ".agent_parent", + "AgentSpec": ".agent_spec", + "AlibabaModelProvider": ".alibaba_model_provider", + "AnthropicModelProvider": ".anthropic_model_provider", + "ApprovalAllow": ".approval_allow", + "ApprovalDecision": ".approval_decision", + "ApprovalDeny": ".approval_deny", + "AskUserQuestionsConfig": ".ask_user_questions_config", + "AvailableMcpServer": ".available_mcp_server", + "AvailableModel": ".available_model", + "AvailableModelProvider": ".available_model_provider", + "AvailableSkill": ".available_skill", + "BaseMcpAuthRequiredEvent": ".base_mcp_auth_required_event", + "BaseThreadDoneEvent": ".base_thread_done_event", + "CancelSessionResponse": ".cancel_session_response", + "CapabilitiesData": ".capabilities_data", + "CatalogCustomModelProvider": ".catalog_custom_model_provider", + "CatalogMcpServer": ".catalog_mcp_server", + "CatalogMcpServerType": ".catalog_mcp_server_type", + "CatalogModel": ".catalog_model", + "CatalogModelProvider": ".catalog_model_provider", + "CatalogSandboxProvider": ".catalog_sandbox_provider", + "CatalogSkill": ".catalog_skill", + "CatalogSkillType": ".catalog_skill_type", + "CatalogWellKnownModelProvider": ".catalog_well_known_model_provider", + "CatalogWellKnownModelProviderType": ".catalog_well_known_model_provider_type", + "ChatCompletionChunkDeltaToolCall": ".chat_completion_chunk_delta_tool_call", + "ChatCompletionChunkDeltaToolCallFunction": ".chat_completion_chunk_delta_tool_call_function", + "ChatCompletionContentPartRefusal": ".chat_completion_content_part_refusal", + "ChatCompletionContentPartText": ".chat_completion_content_part_text", + "ChatCompletionMessageToolCall": ".chat_completion_message_tool_call", + "ChatCompletionMessageToolCallFunction": ".chat_completion_message_tool_call_function", + "CompactionConfig": ".compaction_config", + "ConfiguredMcpServer": ".configured_mcp_server", + "ConfiguredModel": ".configured_model", + "ConfiguredModelProvider": ".configured_model_provider", + "ConfiguredSandboxProvider": ".configured_sandbox_provider", + "ConfiguredSkill": ".configured_skill", + "ContextManagementConfig": ".context_management_config", + "CreateScheduleRunResponse": ".create_schedule_run_response", + "CreateSessionAgent": ".create_session_agent", + "CreatedBySubject": ".created_by_subject", + "CronExpression": ".cron_expression", + "CustomModelProvider": ".custom_model_provider", + "DaytonaSandboxProviderAuth": ".daytona_sandbox_provider_auth", + "DeleteAgentResponse": ".delete_agent_response", + "DeleteScheduleResponse": ".delete_schedule_response", + "DynamicSubAgentsConfig": ".dynamic_sub_agents_config", + "ExtendedChunkDeltaToolCall": ".extended_chunk_delta_tool_call", + "FileContent": ".file_content", + "FinishReason": ".finish_reason", + "FireworksModelProvider": ".fireworks_model_provider", + "GenerativeUiConfig": ".generative_ui_config", + "GetAgentCodeSnippetsResponse": ".get_agent_code_snippets_response", + "GetAgentResponse": ".get_agent_response", + "GetAvailableMcpServerResponse": ".get_available_mcp_server_response", + "GetCapabilitiesResponse": ".get_capabilities_response", + "GetMcpServerCatalogResponse": ".get_mcp_server_catalog_response", + "GetMcpServerResponse": ".get_mcp_server_response", + "GetMeResponse": ".get_me_response", + "GetMeSubject": ".get_me_subject", + "GetModelProviderCatalogResponse": ".get_model_provider_catalog_response", + "GetModelProviderResponse": ".get_model_provider_response", + "GetSandboxProviderCatalogResponse": ".get_sandbox_provider_catalog_response", + "GetSandboxProviderResponse": ".get_sandbox_provider_response", + "GetScheduleResponse": ".get_schedule_response", + "GetSessionMetricsChartDataResponse": ".get_session_metrics_chart_data_response", + "GetSessionMetricsChartResponse": ".get_session_metrics_chart_response", + "GetSessionMetricsMeterResponse": ".get_session_metrics_meter_response", + "GetSessionResponse": ".get_session_response", + "GetSkillCatalogResponse": ".get_skill_catalog_response", + "GetSkillResponse": ".get_skill_response", + "GetTurnResponse": ".get_turn_response", + "GitSkill": ".git_skill", + "GoogleGeminiModelProvider": ".google_gemini_model_provider", + "InitialUserMessage": ".initial_user_message", + "InputTokensCompactionTrigger": ".input_tokens_compaction_trigger", + "LargeToolResponseConfig": ".large_tool_response_config", + "ListAgentsResponse": ".list_agents_response", + "ListAvailableMcpServersResponse": ".list_available_mcp_servers_response", + "ListAvailableModelsResponse": ".list_available_models_response", + "ListAvailableSkillsResponse": ".list_available_skills_response", + "ListMcpServerToolsResponse": ".list_mcp_server_tools_response", + "ListMcpServersResponse": ".list_mcp_servers_response", + "ListModelProvidersResponse": ".list_model_providers_response", + "ListPermissionsResponse": ".list_permissions_response", + "ListScheduleRunsResponse": ".list_schedule_runs_response", + "ListSchedulesResponse": ".list_schedules_response", + "ListSessionEventsResponse": ".list_session_events_response", + "ListSessionsOrder": ".list_sessions_order", + "ListSessionsResponse": ".list_sessions_response", + "ListSkillVersionsResponse": ".list_skill_versions_response", + "ListSkillsResponse": ".list_skills_response", + "ListTurnEventsOrder": ".list_turn_events_order", + "ListTurnEventsResponse": ".list_turn_events_response", + "ListTurnsResponse": ".list_turns_response", + "McpAuthRequiredEvent": ".mcp_auth_required_event", + "McpAuthStatus": ".mcp_auth_status", + "McpAuthStatusStatus": ".mcp_auth_status_status", + "McpInitializeEvent": ".mcp_initialize_event", + "McpServer": ".mcp_server", + "McpServerApprovalToolSelector": ".mcp_server_approval_tool_selector", + "McpServerAuthInfo": ".mcp_server_auth_info", + "McpServerAuthPublic": ".mcp_server_auth_public", + "McpServerAuthPublicOne": ".mcp_server_auth_public_one", + "McpServerAuthPublicZero": ".mcp_server_auth_public_zero", + "McpServerDcrAuth": ".mcp_server_dcr_auth", + "McpServerHeaderAuth": ".mcp_server_header_auth", + "McpServerInitInfo": ".mcp_server_init_info", + "McpServerInitInfoTransportType": ".mcp_server_init_info_transport_type", + "McpServerManifest": ".mcp_server_manifest", + "McpServerManifestAuth": ".mcp_server_manifest_auth", + "McpServerToolSelector": ".mcp_server_tool_selector", + "McpToolInfo": ".mcp_tool_info", + "Me": ".me", + "MeSessionType": ".me_session_type", + "MetricsUnit": ".metrics_unit", + "Model": ".model", + "ModelMessageDeltaEvent": ".model_message_delta_event", + "ModelMessageEvent": ".model_message_event", + "ModelMessageEventContent": ".model_message_event_content", + "ModelMessageEventContentOneItem": ".model_message_event_content_one_item", + "ModelMessageUsage": ".model_message_usage", + "ModelMessageUsageInputTokensBreakdown": ".model_message_usage_input_tokens_breakdown", + "ModelParams": ".model_params", + "ModelProperties": ".model_properties", + "ModelProviderAuth": ".model_provider_auth", + "ModelProviderManifest": ".model_provider_manifest", + "MoonshotModelProvider": ".moonshot_model_provider", + "OpenAiModelProvider": ".open_ai_model_provider", + "PermissionResourceType": ".permission_resource_type", + "PreviousTurnIdInput": ".previous_turn_id_input", + "RawToolCall": ".raw_tool_call", + "ReasoningEffort": ".reasoning_effort", + "RemoteMcpServerManifest": ".remote_mcp_server_manifest", + "RequestErrorResponse": ".request_error_response", + "RequestErrorResponseError": ".request_error_response_error", + "ResourceName": ".resource_name", + "ResourcePermission": ".resource_permission", + "ResponseFormat": ".response_format", + "ResponseFormatJsonObject": ".response_format_json_object", + "ResponseFormatJsonSchema": ".response_format_json_schema", + "ResponseFormatJsonSchemaJsonSchema": ".response_format_json_schema_json_schema", + "ResponseFormatText": ".response_format_text", + "RuntimeConfig": ".runtime_config", + "SandboxBuildStatus": ".sandbox_build_status", + "SandboxCapability": ".sandbox_capability", + "SandboxConfig": ".sandbox_config", + "SandboxCreatedEvent": ".sandbox_created_event", + "SandboxProviderManifest": ".sandbox_provider_manifest", + "Schedule": ".schedule", + "ScheduleManifest": ".schedule_manifest", + "ScheduleRun": ".schedule_run", + "ScheduleRunStatus": ".schedule_run_status", + "ScheduleStatus": ".schedule_status", + "Session": ".session", + "SessionAgent": ".session_agent", + "SessionAgentInline": ".session_agent_inline", + "SessionAgentNameRef": ".session_agent_name_ref", + "SessionAgentReference": ".session_agent_reference", + "SessionAgentSpecBody": ".session_agent_spec_body", + "SessionEvent": ".session_event", + "SessionEventItem": ".session_event_item", + "SessionMetadata": ".session_metadata", + "SessionMetrics": ".session_metrics", + "SessionMetricsChart": ".session_metrics_chart", + "SessionMetricsChartDataResponse": ".session_metrics_chart_data_response", + "SessionMetricsChartName": ".session_metrics_chart_name", + "SessionMetricsChartResponse": ".session_metrics_chart_response", + "SessionMetricsGraph": ".session_metrics_graph", + "SessionMetricsGraphLine": ".session_metrics_graph_line", + "SessionMetricsMeter": ".session_metrics_meter", + "SessionMetricsMeterName": ".session_metrics_meter_name", + "SessionMetricsMeterResponse": ".session_metrics_meter_response", + "SessionMetricsPoint": ".session_metrics_point", + "SessionSource": ".session_source", + "SessionSourceSchedule": ".session_source_schedule", + "SessionSourceType": ".session_source_type", + "SettingsCapability": ".settings_capability", + "Skill": ".skill", + "SkillCapability": ".skill_capability", + "SkillManifest": ".skill_manifest", + "SkillVersion": ".skill_version", + "TextContent": ".text_content", + "ThreadCreatedEvent": ".thread_created_event", + "ThreadDoneEvent": ".thread_done_event", + "ThreadState": ".thread_state", + "ThreadStateDone": ".thread_state_done", + "ThreadStateError": ".thread_state_error", + "Timezone": ".timezone", + "TogetherAiModelProvider": ".together_ai_model_provider", + "TokenPagination": ".token_pagination", + "ToolApprovalRequiredEvent": ".tool_approval_required_event", + "ToolCall": ".tool_call", + "ToolCallRef": ".tool_call_ref", + "ToolInfo": ".tool_info", + "ToolResponseEvent": ".tool_response_event", + "ToolResponseRequiredEvent": ".tool_response_required_event", + "TrueFoundryMcpServerManifest": ".true_foundry_mcp_server_manifest", + "TrueFoundryModelProvider": ".true_foundry_model_provider", + "TrueFoundryRegistrySkill": ".true_foundry_registry_skill", + "TrueFoundrySystemToolInfo": ".true_foundry_system_tool_info", + "Turn": ".turn", + "TurnCreatedEvent": ".turn_created_event", + "TurnDoneEvent": ".turn_done_event", + "TurnDoneEventState": ".turn_done_event_state", + "TurnInputItem": ".turn_input_item", + "TurnMetrics": ".turn_metrics", + "TurnState": ".turn_state", + "TurnStateCancelled": ".turn_state_cancelled", + "TurnStateCancelledMetrics": ".turn_state_cancelled_metrics", + "TurnStateCancelledReason": ".turn_state_cancelled_reason", + "TurnStateDone": ".turn_state_done", + "TurnStateError": ".turn_state_error", + "TurnStateErrorMetrics": ".turn_state_error_metrics", + "TurnStateRunning": ".turn_state_running", + "TurnStreamingEvent": ".turn_streaming_event", + "UserMessage": ".user_message", + "UserMessageContent": ".user_message_content", + "UserMessageContentItem": ".user_message_content_item", + "UserToolApprovalEvent": ".user_tool_approval_event", + "UserToolResponseEvent": ".user_tool_response_event", + "ZaiModelProvider": ".zai_model_provider", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ActionRequiredEvent", + "Agent", + "AgentCodeSnippet", + "AgentCodeSnippetSampleCode", + "AgentCodeSnippets", + "AgentInfo", + "AgentParent", + "AgentSpec", + "AlibabaModelProvider", + "AnthropicModelProvider", + "ApprovalAllow", + "ApprovalDecision", + "ApprovalDeny", + "AskUserQuestionsConfig", + "AvailableMcpServer", + "AvailableModel", + "AvailableModelProvider", + "AvailableSkill", + "BaseMcpAuthRequiredEvent", + "BaseThreadDoneEvent", + "CancelSessionResponse", + "CapabilitiesData", + "CatalogCustomModelProvider", + "CatalogMcpServer", + "CatalogMcpServerType", + "CatalogModel", + "CatalogModelProvider", + "CatalogSandboxProvider", + "CatalogSkill", + "CatalogSkillType", + "CatalogWellKnownModelProvider", + "CatalogWellKnownModelProviderType", + "ChatCompletionChunkDeltaToolCall", + "ChatCompletionChunkDeltaToolCallFunction", + "ChatCompletionContentPartRefusal", + "ChatCompletionContentPartText", + "ChatCompletionMessageToolCall", + "ChatCompletionMessageToolCallFunction", + "CompactionConfig", + "ConfiguredMcpServer", + "ConfiguredModel", + "ConfiguredModelProvider", + "ConfiguredSandboxProvider", + "ConfiguredSkill", + "ContextManagementConfig", + "CreateScheduleRunResponse", + "CreateSessionAgent", + "CreatedBySubject", + "CronExpression", + "CustomModelProvider", + "DaytonaSandboxProviderAuth", + "DeleteAgentResponse", + "DeleteScheduleResponse", + "DynamicSubAgentsConfig", + "ExtendedChunkDeltaToolCall", + "FileContent", + "FinishReason", + "FireworksModelProvider", + "GenerativeUiConfig", + "GetAgentCodeSnippetsResponse", + "GetAgentResponse", + "GetAvailableMcpServerResponse", + "GetCapabilitiesResponse", + "GetMcpServerCatalogResponse", + "GetMcpServerResponse", + "GetMeResponse", + "GetMeSubject", + "GetModelProviderCatalogResponse", + "GetModelProviderResponse", + "GetSandboxProviderCatalogResponse", + "GetSandboxProviderResponse", + "GetScheduleResponse", + "GetSessionMetricsChartDataResponse", + "GetSessionMetricsChartResponse", + "GetSessionMetricsMeterResponse", + "GetSessionResponse", + "GetSkillCatalogResponse", + "GetSkillResponse", + "GetTurnResponse", + "GitSkill", + "GoogleGeminiModelProvider", + "InitialUserMessage", + "InputTokensCompactionTrigger", + "LargeToolResponseConfig", + "ListAgentsResponse", + "ListAvailableMcpServersResponse", + "ListAvailableModelsResponse", + "ListAvailableSkillsResponse", + "ListMcpServerToolsResponse", + "ListMcpServersResponse", + "ListModelProvidersResponse", + "ListPermissionsResponse", + "ListScheduleRunsResponse", + "ListSchedulesResponse", + "ListSessionEventsResponse", + "ListSessionsOrder", + "ListSessionsResponse", + "ListSkillVersionsResponse", + "ListSkillsResponse", + "ListTurnEventsOrder", + "ListTurnEventsResponse", + "ListTurnsResponse", + "McpAuthRequiredEvent", + "McpAuthStatus", + "McpAuthStatusStatus", + "McpInitializeEvent", + "McpServer", + "McpServerApprovalToolSelector", + "McpServerAuthInfo", + "McpServerAuthPublic", + "McpServerAuthPublicOne", + "McpServerAuthPublicZero", + "McpServerDcrAuth", + "McpServerHeaderAuth", + "McpServerInitInfo", + "McpServerInitInfoTransportType", + "McpServerManifest", + "McpServerManifestAuth", + "McpServerToolSelector", + "McpToolInfo", + "Me", + "MeSessionType", + "MetricsUnit", + "Model", + "ModelMessageDeltaEvent", + "ModelMessageEvent", + "ModelMessageEventContent", + "ModelMessageEventContentOneItem", + "ModelMessageUsage", + "ModelMessageUsageInputTokensBreakdown", + "ModelParams", + "ModelProperties", + "ModelProviderAuth", + "ModelProviderManifest", + "MoonshotModelProvider", + "OpenAiModelProvider", + "PermissionResourceType", + "PreviousTurnIdInput", + "RawToolCall", + "ReasoningEffort", + "RemoteMcpServerManifest", + "RequestErrorResponse", + "RequestErrorResponseError", + "ResourceName", + "ResourcePermission", + "ResponseFormat", + "ResponseFormatJsonObject", + "ResponseFormatJsonSchema", + "ResponseFormatJsonSchemaJsonSchema", + "ResponseFormatText", + "RuntimeConfig", + "SandboxBuildStatus", + "SandboxCapability", + "SandboxConfig", + "SandboxCreatedEvent", + "SandboxProviderManifest", + "Schedule", + "ScheduleManifest", + "ScheduleRun", + "ScheduleRunStatus", + "ScheduleStatus", + "Session", + "SessionAgent", + "SessionAgentInline", + "SessionAgentNameRef", + "SessionAgentReference", + "SessionAgentSpecBody", + "SessionEvent", + "SessionEventItem", + "SessionMetadata", + "SessionMetrics", + "SessionMetricsChart", + "SessionMetricsChartDataResponse", + "SessionMetricsChartName", + "SessionMetricsChartResponse", + "SessionMetricsGraph", + "SessionMetricsGraphLine", + "SessionMetricsMeter", + "SessionMetricsMeterName", + "SessionMetricsMeterResponse", + "SessionMetricsPoint", + "SessionSource", + "SessionSourceSchedule", + "SessionSourceType", + "SettingsCapability", + "Skill", + "SkillCapability", + "SkillManifest", + "SkillVersion", + "TextContent", + "ThreadCreatedEvent", + "ThreadDoneEvent", + "ThreadState", + "ThreadStateDone", + "ThreadStateError", + "Timezone", + "TogetherAiModelProvider", + "TokenPagination", + "ToolApprovalRequiredEvent", + "ToolCall", + "ToolCallRef", + "ToolInfo", + "ToolResponseEvent", + "ToolResponseRequiredEvent", + "TrueFoundryMcpServerManifest", + "TrueFoundryModelProvider", + "TrueFoundryRegistrySkill", + "TrueFoundrySystemToolInfo", + "Turn", + "TurnCreatedEvent", + "TurnDoneEvent", + "TurnDoneEventState", + "TurnInputItem", + "TurnMetrics", + "TurnState", + "TurnStateCancelled", + "TurnStateCancelledMetrics", + "TurnStateCancelledReason", + "TurnStateDone", + "TurnStateError", + "TurnStateErrorMetrics", + "TurnStateRunning", + "TurnStreamingEvent", + "UserMessage", + "UserMessageContent", + "UserMessageContentItem", + "UserToolApprovalEvent", + "UserToolResponseEvent", + "ZaiModelProvider", +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/action_required_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/action_required_event.py new file mode 100644 index 000000000..31aa07509 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/action_required_event.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .mcp_auth_required_event import McpAuthRequiredEvent +from .tool_approval_required_event import ToolApprovalRequiredEvent +from .tool_response_required_event import ToolResponseRequiredEvent + +ActionRequiredEvent = typing.Union[McpAuthRequiredEvent, ToolApprovalRequiredEvent, ToolResponseRequiredEvent] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent.py new file mode 100644 index 000000000..0289e1fd2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_spec import AgentSpec +from .created_by_subject import CreatedBySubject +from .resource_name import ResourceName + + +class Agent(UncheckedBaseModel): + created_by_subject: CreatedBySubject + description: str + id: str = pydantic.Field() + """ + Immutable server-generated agent identifier. + """ + + manifest: AgentSpec + name: ResourceName + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippet.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippet.py new file mode 100644 index 000000000..ea4b52e6a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippet.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_code_snippet_sample_code import AgentCodeSnippetSampleCode + + +class AgentCodeSnippet(UncheckedBaseModel): + icon: str + label_name: str + language: str + sample_code: AgentCodeSnippetSampleCode + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippet_sample_code.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippet_sample_code.py new file mode 100644 index 000000000..e9067196c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippet_sample_code.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AgentCodeSnippetSampleCode(UncheckedBaseModel): + non_stream: str = pydantic.Field() + """ + SDK sample that creates a turn without streaming. + """ + + stream: str = pydantic.Field() + """ + SDK sample that streams turn events. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippets.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippets.py new file mode 100644 index 000000000..072e6cc59 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent_code_snippets.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_code_snippet import AgentCodeSnippet + + +class AgentCodeSnippets(UncheckedBaseModel): + base_url: str = pydantic.Field() + """ + Public base URL for the TrueForge SDK `baseUrl`. + """ + + snippets: typing.List[AgentCodeSnippet] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent_info.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent_info.py new file mode 100644 index 000000000..53a14069f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent_info.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AgentInfo(UncheckedBaseModel): + input: str = pydantic.Field() + """ + Input prompt passed to the subagent. + """ + + model: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional model override for the subagent. + """ + + name: str = pydantic.Field() + """ + Display name of the dynamic subagent. + """ + + type: typing.Literal["dynamic"] = pydantic.Field(default="dynamic") + """ + Subagent kind. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent_parent.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent_parent.py new file mode 100644 index 000000000..0ffcdbdaa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent_parent.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AgentParent(UncheckedBaseModel): + thread_id: str = pydantic.Field() + """ + Parent thread that spawned the child agent. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call on the parent thread that created the child. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/agent_spec.py b/python/trueforge_sdk/src/trueforge_sdk/types/agent_spec.py new file mode 100644 index 000000000..c009ee0ca --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/agent_spec.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .initial_user_message import InitialUserMessage +from .mcp_server import McpServer +from .model import Model +from .response_format import ResponseFormat +from .runtime_config import RuntimeConfig +from .skill import Skill + + +class AgentSpec(UncheckedBaseModel): + """ + Complete agent definition used inline on a session or saved as a named agent. + """ + + config: typing.Optional[RuntimeConfig] = None + instructions: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional system prompt — the agent's role, behavior, and constraints. + """ + + mcp_servers: typing.Optional[typing.List[McpServer]] = pydantic.Field(default=None) + """ + Optional MCP servers attached by configured name. + """ + + messages: typing.Optional[typing.List[InitialUserMessage]] = pydantic.Field(default=None) + """ + Optional initial user messages injected at the start of every session. + """ + + model: Model + response_format: typing.Optional[ResponseFormat] = None + skills: typing.Optional[typing.List[Skill]] = pydantic.Field(default=None) + """ + Skills used in this agent. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/alibaba_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/alibaba_model_provider.py new file mode 100644 index 000000000..d5f6a6e72 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/alibaba_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class AlibabaModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://dashscope-intl.aliyuncs.com/compatible-mode/v1") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["alibaba"] = "alibaba" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/anthropic_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/anthropic_model_provider.py new file mode 100644 index 000000000..7330aeae7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/anthropic_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class AnthropicModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://api.anthropic.com/v1") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["anthropic"] = "anthropic" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/approval_allow.py b/python/trueforge_sdk/src/trueforge_sdk/types/approval_allow.py new file mode 100644 index 000000000..6ae4bb598 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/approval_allow.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ApprovalAllow(UncheckedBaseModel): + status: typing.Literal["allow"] = "allow" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/approval_decision.py b/python/trueforge_sdk/src/trueforge_sdk/types/approval_decision.py new file mode 100644 index 000000000..aa156aef1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/approval_decision.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .approval_allow import ApprovalAllow +from .approval_deny import ApprovalDeny + +ApprovalDecision = typing.Union[ApprovalAllow, ApprovalDeny] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/approval_deny.py b/python/trueforge_sdk/src/trueforge_sdk/types/approval_deny.py new file mode 100644 index 000000000..0c178afcc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/approval_deny.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ApprovalDeny(UncheckedBaseModel): + reason: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional reason shown to the agent when denied. + """ + + status: typing.Literal["deny"] = "deny" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/ask_user_questions_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/ask_user_questions_config.py new file mode 100644 index 000000000..300b53568 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/ask_user_questions_config.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AskUserQuestionsConfig(UncheckedBaseModel): + enabled: typing.Optional[bool] = pydantic.Field(default=True) + """ + Enable the `ask_user_question` tool. Default: true. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/available_mcp_server.py b/python/trueforge_sdk/src/trueforge_sdk/types/available_mcp_server.py new file mode 100644 index 000000000..f8e469969 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/available_mcp_server.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_auth_status import McpAuthStatus +from .mcp_server_auth_public import McpServerAuthPublic +from .resource_name import ResourceName + + +class AvailableMcpServer(UncheckedBaseModel): + auth: typing.Optional[McpServerAuthPublic] = None + auth_status: McpAuthStatus + name: ResourceName + url: str = pydantic.Field() + """ + URL of the remote MCP server. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/available_model.py b/python/trueforge_sdk/src/trueforge_sdk/types/available_model.py new file mode 100644 index 000000000..c683de00d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/available_model.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .available_model_provider import AvailableModelProvider +from .model_properties import ModelProperties + + +class AvailableModel(UncheckedBaseModel): + model_id: str = pydantic.Field() + """ + Upstream, provider-specific identifier sent to the provider API. + """ + + name: str = pydantic.Field() + """ + Fully qualified name `provider_name/model_name`, e.g. "openai/gpt-5-6-sol". Unique within a tenant. + """ + + properties: ModelProperties + provider: AvailableModelProvider + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/available_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/available_model_provider.py new file mode 100644 index 000000000..42b21567e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/available_model_provider.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AvailableModelProvider(UncheckedBaseModel): + """ + Owning configured provider. + """ + + name: str = pydantic.Field() + """ + Configured provider resource name; matches the FQN prefix of `name`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/available_skill.py b/python/trueforge_sdk/src/trueforge_sdk/types/available_skill.py new file mode 100644 index 000000000..60f4e681f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/available_skill.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AvailableSkill(UncheckedBaseModel): + description: str = pydantic.Field() + """ + Concise guidance for when the agent should use the skill. + """ + + metadata: typing.Optional[typing.Dict[str, str]] = pydantic.Field(default=None) + """ + Optional skill metadata. + """ + + name: str = pydantic.Field() + """ + Skill name. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/base_mcp_auth_required_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/base_mcp_auth_required_event.py new file mode 100644 index 000000000..fb47a1bf7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/base_mcp_auth_required_event.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class BaseMcpAuthRequiredEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Always null — this is a run-level event. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/base_thread_done_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/base_thread_done_event.py new file mode 100644 index 000000000..2b604a655 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/base_thread_done_event.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_parent import AgentParent + + +class BaseThreadDoneEvent(UncheckedBaseModel): + parent: typing.Optional[AgentParent] = None + thread_id: str = pydantic.Field() + """ + Thread that finished. + """ + + title: str = pydantic.Field() + """ + Human-readable thread title. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/cancel_session_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/cancel_session_response.py new file mode 100644 index 000000000..92b4a1679 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/cancel_session_response.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CancelSessionResponse(UncheckedBaseModel): + """ + Empty success body. HTTP 200 means the cancel request was accepted (or nothing was running). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/capabilities_data.py b/python/trueforge_sdk/src/trueforge_sdk/types/capabilities_data.py new file mode 100644 index 000000000..fe095fd67 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/capabilities_data.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .sandbox_capability import SandboxCapability +from .settings_capability import SettingsCapability +from .skill_capability import SkillCapability + + +class CapabilitiesData(UncheckedBaseModel): + sandbox: SandboxCapability + settings: SettingsCapability + skill: SkillCapability + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_custom_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_custom_model_provider.py new file mode 100644 index 000000000..300bafe73 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_custom_model_provider.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .reasoning_effort import ReasoningEffort + + +class CatalogCustomModelProvider(UncheckedBaseModel): + supported_reasoning_efforts: typing.List[ReasoningEffort] = pydantic.Field() + """ + Supported reasoning-effort values for this provider + """ + + type: typing.Literal["custom"] = "custom" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_mcp_server.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_mcp_server.py new file mode 100644 index 000000000..2ad3159f5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_mcp_server.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_mcp_server_type import CatalogMcpServerType +from .mcp_server_manifest_auth import McpServerManifestAuth +from .resource_name import ResourceName + + +class CatalogMcpServer(UncheckedBaseModel): + auth: typing.Optional[McpServerManifestAuth] = None + description: str = pydantic.Field() + """ + Concise summary of what this MCP server provides. + """ + + logo: typing.Optional[str] = pydantic.Field(default=None) + """ + URL of the MCP server logo asset. + """ + + name: ResourceName + type: CatalogMcpServerType = "remote" + url: str = pydantic.Field() + """ + URL of the remote MCP server. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_mcp_server_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_mcp_server_type.py new file mode 100644 index 000000000..cb0af3c8c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_mcp_server_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +CatalogMcpServerType = typing.Literal["remote"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_model.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_model.py new file mode 100644 index 000000000..3005a1b61 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_model.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_properties import ModelProperties +from .resource_name import ResourceName + + +class CatalogModel(UncheckedBaseModel): + model_id: str = pydantic.Field() + """ + Upstream, provider-specific identifier sent to the provider API. + """ + + name: ResourceName + properties: ModelProperties + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_model_provider.py new file mode 100644 index 000000000..922ed127b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_model_provider.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .catalog_custom_model_provider import CatalogCustomModelProvider +from .catalog_well_known_model_provider import CatalogWellKnownModelProvider + +CatalogModelProvider = typing.Union[CatalogWellKnownModelProvider, CatalogCustomModelProvider] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_sandbox_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_sandbox_provider.py new file mode 100644 index 000000000..eb5c01ba2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_sandbox_provider.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CatalogSandboxProvider(UncheckedBaseModel): + auto_archive_interval_in_minutes: int = pydantic.Field() + """ + Minutes before Daytona auto-archives the sandbox (0 disables). + """ + + auto_delete_interval_in_minutes: int = pydantic.Field() + """ + Minutes before Daytona auto-deletes the sandbox (0 disables). + """ + + auto_stop_interval_in_minutes: int = pydantic.Field() + """ + Minutes of idle time before Daytona auto-stops the sandbox (0 disables). + """ + + exec_timeout_ms: int = pydantic.Field() + """ + Default sandbox command exec timeout in milliseconds. + """ + + type: typing.Literal["daytona"] = pydantic.Field(default="daytona") + """ + Daytona sandbox provider. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_skill.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_skill.py new file mode 100644 index 000000000..9c4f58468 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_skill.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_skill_type import CatalogSkillType +from .resource_name import ResourceName + + +class CatalogSkill(UncheckedBaseModel): + description: str = pydantic.Field() + """ + Concise guidance for when the agent should use the skill. + """ + + name: ResourceName + path: typing.Optional[str] = pydantic.Field(default=None) + """ + Path to the skill directory within the repository. Omit to use the repository root. + """ + + ref: str = pydantic.Field() + """ + Git ref — branch name, tag, or commit SHA. + """ + + type: CatalogSkillType = "git" + url: str = pydantic.Field() + """ + Full HTTPS URL of a GitHub or GitLab repository. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_skill_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_skill_type.py new file mode 100644 index 000000000..e403da868 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_skill_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +CatalogSkillType = typing.Literal["git"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_well_known_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_well_known_model_provider.py new file mode 100644 index 000000000..cca86bc73 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_well_known_model_provider.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_model import CatalogModel +from .catalog_well_known_model_provider_type import CatalogWellKnownModelProviderType + + +class CatalogWellKnownModelProvider(UncheckedBaseModel): + logo: typing.Optional[str] = pydantic.Field(default=None) + """ + URL of the provider logo asset + """ + + models: typing.List[CatalogModel] = pydantic.Field() + """ + Preset models + """ + + type: CatalogWellKnownModelProviderType + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_well_known_model_provider_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_well_known_model_provider_type.py new file mode 100644 index 000000000..a159d9dda --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_well_known_model_provider_type.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class CatalogWellKnownModelProviderType(enum.StrEnum): + OPENAI = "openai" + ANTHROPIC = "anthropic" + GOOGLE_GEMINI = "google-gemini" + FIREWORKS = "fireworks" + ZAI = "zai" + MOONSHOT = "moonshot" + ALIBABA = "alibaba" + TOGETHER = "together" + _UNKNOWN = "__CATALOGWELLKNOWNMODELPROVIDERTYPE_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "CatalogWellKnownModelProviderType": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + openai: typing.Callable[[], T_Result], + anthropic: typing.Callable[[], T_Result], + google_gemini: typing.Callable[[], T_Result], + fireworks: typing.Callable[[], T_Result], + zai: typing.Callable[[], T_Result], + moonshot: typing.Callable[[], T_Result], + alibaba: typing.Callable[[], T_Result], + together: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is CatalogWellKnownModelProviderType.OPENAI: + return openai() + if self is CatalogWellKnownModelProviderType.ANTHROPIC: + return anthropic() + if self is CatalogWellKnownModelProviderType.GOOGLE_GEMINI: + return google_gemini() + if self is CatalogWellKnownModelProviderType.FIREWORKS: + return fireworks() + if self is CatalogWellKnownModelProviderType.ZAI: + return zai() + if self is CatalogWellKnownModelProviderType.MOONSHOT: + return moonshot() + if self is CatalogWellKnownModelProviderType.ALIBABA: + return alibaba() + if self is CatalogWellKnownModelProviderType.TOGETHER: + return together() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_chunk_delta_tool_call.py b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_chunk_delta_tool_call.py new file mode 100644 index 000000000..11cd11e5d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_chunk_delta_tool_call.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .chat_completion_chunk_delta_tool_call_function import ChatCompletionChunkDeltaToolCallFunction + + +class ChatCompletionChunkDeltaToolCall(UncheckedBaseModel): + function: typing.Optional[ChatCompletionChunkDeltaToolCallFunction] = None + id: typing.Optional[str] = pydantic.Field(default=None) + """ + Tool call id (may arrive across multiple deltas). + """ + + index: int = pydantic.Field() + """ + Index of this tool call in the streaming delta array. + """ + + type: typing.Optional[typing.Literal["function"]] = pydantic.Field(default=None) + """ + Tool call type when present on this delta. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_chunk_delta_tool_call_function.py b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_chunk_delta_tool_call_function.py new file mode 100644 index 000000000..fd81e9027 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_chunk_delta_tool_call_function.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ChatCompletionChunkDeltaToolCallFunction(UncheckedBaseModel): + arguments: typing.Optional[str] = pydantic.Field(default=None) + """ + Partial or complete JSON arguments string. + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Partial or complete function name. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_content_part_refusal.py b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_content_part_refusal.py new file mode 100644 index 000000000..9ae46a100 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_content_part_refusal.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ChatCompletionContentPartRefusal(UncheckedBaseModel): + refusal: str = pydantic.Field() + """ + Refusal message text. + """ + + type: typing.Literal["refusal"] = pydantic.Field(default="refusal") + """ + Model refusal content part. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_content_part_text.py b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_content_part_text.py new file mode 100644 index 000000000..466bb6afa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_content_part_text.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ChatCompletionContentPartText(UncheckedBaseModel): + text: str = pydantic.Field() + """ + Plain-text content. + """ + + type: typing.Literal["text"] = pydantic.Field(default="text") + """ + Text content part. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_message_tool_call.py b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_message_tool_call.py new file mode 100644 index 000000000..83e6b8e60 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_message_tool_call.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .chat_completion_message_tool_call_function import ChatCompletionMessageToolCallFunction + + +class ChatCompletionMessageToolCall(UncheckedBaseModel): + function: ChatCompletionMessageToolCallFunction + id: str = pydantic.Field() + """ + Tool call id. + """ + + type: typing.Literal["function"] = pydantic.Field(default="function") + """ + Tool call type. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_message_tool_call_function.py b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_message_tool_call_function.py new file mode 100644 index 000000000..7aece126f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/chat_completion_message_tool_call_function.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ChatCompletionMessageToolCallFunction(UncheckedBaseModel): + arguments: str = pydantic.Field() + """ + JSON-encoded function arguments string. + """ + + name: str = pydantic.Field() + """ + Function/tool name. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/compaction_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/compaction_config.py new file mode 100644 index 000000000..dade981e5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/compaction_config.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .input_tokens_compaction_trigger import InputTokensCompactionTrigger + + +class CompactionConfig(UncheckedBaseModel): + """ + Uses 80% of the model context length when the explicit trigger is omitted, or 50000 tokens if unknown. + """ + + enabled: typing.Optional[bool] = pydantic.Field(default=True) + """ + Summarize older history when context grows too large. Default: true. + """ + + trigger: typing.Optional[InputTokensCompactionTrigger] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_mcp_server.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_mcp_server.py new file mode 100644 index 000000000..7af13dbae --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_mcp_server.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_auth_status import McpAuthStatus +from .mcp_server_manifest import McpServerManifest +from .resource_name import ResourceName + + +class ConfiguredMcpServer(UncheckedBaseModel): + auth_status: McpAuthStatus + manifest: McpServerManifest + name: ResourceName + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_model.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_model.py new file mode 100644 index 000000000..2581d0817 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_model.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_properties import ModelProperties +from .resource_name import ResourceName + + +class ConfiguredModel(UncheckedBaseModel): + model_id: str = pydantic.Field() + """ + Upstream, provider-specific identifier sent to the provider API. + """ + + name: ResourceName + properties: ModelProperties + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_model_provider.py new file mode 100644 index 000000000..c239d8baa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_model_provider.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_provider_manifest import ModelProviderManifest +from .resource_name import ResourceName + + +class ConfiguredModelProvider(UncheckedBaseModel): + manifest: ModelProviderManifest + name: ResourceName + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py new file mode 100644 index 000000000..557c21dc9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .sandbox_build_status import SandboxBuildStatus +from .sandbox_provider_manifest import SandboxProviderManifest + + +class ConfiguredSandboxProvider(UncheckedBaseModel): + manifest: SandboxProviderManifest + status: SandboxBuildStatus + status_reason: typing.Optional[str] = pydantic.Field(default=None) + """ + Human-readable detail for the current status; null when ready. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_skill.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_skill.py new file mode 100644 index 000000000..4d32a7e85 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_skill.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .skill_manifest import SkillManifest + + +class ConfiguredSkill(UncheckedBaseModel): + manifest: SkillManifest + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/context_management_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/context_management_config.py new file mode 100644 index 000000000..c0d5c04f5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/context_management_config.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .compaction_config import CompactionConfig +from .large_tool_response_config import LargeToolResponseConfig + + +class ContextManagementConfig(UncheckedBaseModel): + compaction: typing.Optional[CompactionConfig] = None + large_tool_response: typing.Optional[LargeToolResponseConfig] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/create_schedule_run_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_schedule_run_response.py new file mode 100644 index 000000000..59c98d91e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_schedule_run_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .schedule_run import ScheduleRun + + +class CreateScheduleRunResponse(UncheckedBaseModel): + data: ScheduleRun + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/create_session_agent.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_session_agent.py new file mode 100644 index 000000000..5612f6743 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_session_agent.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .session_agent_name_ref import SessionAgentNameRef +from .session_agent_spec_body import SessionAgentSpecBody + +CreateSessionAgent = typing.Union[SessionAgentNameRef, SessionAgentSpecBody] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/created_by_subject.py b/python/trueforge_sdk/src/trueforge_sdk/types/created_by_subject.py new file mode 100644 index 000000000..4d9c07b15 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/created_by_subject.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CreatedBySubject(UncheckedBaseModel): + """ + Who created this resource. + """ + + subject_display_name: str = pydantic.Field() + """ + Display name. + """ + + subject_id: str = pydantic.Field() + """ + Subject id. + """ + + subject_type: str = pydantic.Field() + """ + Subject type. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/cron_expression.py b/python/trueforge_sdk/src/trueforge_sdk/types/cron_expression.py new file mode 100644 index 000000000..df2cfb232 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/cron_expression.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +CronExpression = str +""" +Standard 5-field cron expression, evaluated in `timezone`. +""" diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/custom_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/custom_model_provider.py new file mode 100644 index 000000000..a1b14e733 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/custom_model_provider.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth +from .resource_name import ResourceName + + +class CustomModelProvider(UncheckedBaseModel): + auth: typing.Optional[ModelProviderAuth] = None + base_url: str = pydantic.Field() + """ + Base URL of the provider's API. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + name: ResourceName + type: typing.Literal["custom"] = "custom" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_provider_auth.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_provider_auth.py new file mode 100644 index 000000000..6be23eb26 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_provider_auth.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DaytonaSandboxProviderAuth(UncheckedBaseModel): + """ + Daytona authentication credentials. + """ + + api_key: str = pydantic.Field() + """ + Daytona API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/delete_agent_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/delete_agent_response.py new file mode 100644 index 000000000..bb7161412 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/delete_agent_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DeleteAgentResponse(UncheckedBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/delete_schedule_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/delete_schedule_response.py new file mode 100644 index 000000000..65dd45ba0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/delete_schedule_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DeleteScheduleResponse(UncheckedBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/dynamic_sub_agents_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/dynamic_sub_agents_config.py new file mode 100644 index 000000000..b9ebd811b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/dynamic_sub_agents_config.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DynamicSubAgentsConfig(UncheckedBaseModel): + enabled: typing.Optional[bool] = pydantic.Field(default=True) + """ + Allow the agent to spawn dynamic subagents. Default: true. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/extended_chunk_delta_tool_call.py b/python/trueforge_sdk/src/trueforge_sdk/types/extended_chunk_delta_tool_call.py new file mode 100644 index 000000000..fc97369f6 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/extended_chunk_delta_tool_call.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from .chat_completion_chunk_delta_tool_call import ChatCompletionChunkDeltaToolCall +from .tool_info import ToolInfo + + +class ExtendedChunkDeltaToolCall(ChatCompletionChunkDeltaToolCall): + provider_specific_fields: typing.Optional[typing.Dict[str, typing.Any]] = None + tool_info: typing.Optional[ToolInfo] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/file_content.py b/python/trueforge_sdk/src/trueforge_sdk/types/file_content.py new file mode 100644 index 000000000..8de0e328f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/file_content.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FileContent(UncheckedBaseModel): + data: str = pydantic.Field() + """ + Data URI: `data:;base64,`. MIME type is parsed from the URI. + """ + + name: str = pydantic.Field() + """ + Filename presented to the agent. + """ + + type: typing.Literal["file"] = "file" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/finish_reason.py b/python/trueforge_sdk/src/trueforge_sdk/types/finish_reason.py new file mode 100644 index 000000000..1f82f503b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/finish_reason.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class FinishReason(enum.StrEnum): + """ + Why the model stopped generating. + """ + + STOP = "stop" + LENGTH = "length" + TOOL_CALLS = "tool_calls" + CONTENT_FILTER = "content_filter" + FUNCTION_CALL = "function_call" + _UNKNOWN = "__FINISHREASON_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "FinishReason": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + stop: typing.Callable[[], T_Result], + length: typing.Callable[[], T_Result], + tool_calls: typing.Callable[[], T_Result], + content_filter: typing.Callable[[], T_Result], + function_call: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is FinishReason.STOP: + return stop() + if self is FinishReason.LENGTH: + return length() + if self is FinishReason.TOOL_CALLS: + return tool_calls() + if self is FinishReason.CONTENT_FILTER: + return content_filter() + if self is FinishReason.FUNCTION_CALL: + return function_call() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/fireworks_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/fireworks_model_provider.py new file mode 100644 index 000000000..19d5f71b8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/fireworks_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class FireworksModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://api.fireworks.ai/inference/v1") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["fireworks"] = "fireworks" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/generative_ui_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/generative_ui_config.py new file mode 100644 index 000000000..9ba42eeb7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/generative_ui_config.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class GenerativeUiConfig(UncheckedBaseModel): + enabled: typing.Optional[bool] = pydantic.Field(default=True) + """ + Enable Generative UI (OpenUI blocks). Default: true. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_agent_code_snippets_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_agent_code_snippets_response.py new file mode 100644 index 000000000..e9769c091 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_agent_code_snippets_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_code_snippets import AgentCodeSnippets + + +class GetAgentCodeSnippetsResponse(UncheckedBaseModel): + data: AgentCodeSnippets + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_agent_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_agent_response.py new file mode 100644 index 000000000..cace76ab4 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_agent_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent import Agent + + +class GetAgentResponse(UncheckedBaseModel): + data: Agent + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_available_mcp_server_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_available_mcp_server_response.py new file mode 100644 index 000000000..07641eef3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_available_mcp_server_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .available_mcp_server import AvailableMcpServer + + +class GetAvailableMcpServerResponse(UncheckedBaseModel): + data: AvailableMcpServer + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_capabilities_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_capabilities_response.py new file mode 100644 index 000000000..7f55ca553 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_capabilities_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .capabilities_data import CapabilitiesData + + +class GetCapabilitiesResponse(UncheckedBaseModel): + data: CapabilitiesData + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_mcp_server_catalog_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_mcp_server_catalog_response.py new file mode 100644 index 000000000..b15a252b1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_mcp_server_catalog_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_mcp_server import CatalogMcpServer + + +class GetMcpServerCatalogResponse(UncheckedBaseModel): + data: typing.List[CatalogMcpServer] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_mcp_server_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_mcp_server_response.py new file mode 100644 index 000000000..12dbd4916 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_mcp_server_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_mcp_server import ConfiguredMcpServer + + +class GetMcpServerResponse(UncheckedBaseModel): + data: ConfiguredMcpServer + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_me_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_me_response.py new file mode 100644 index 000000000..34b34231d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_me_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .me import Me + + +class GetMeResponse(UncheckedBaseModel): + data: Me + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_me_subject.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_me_subject.py new file mode 100644 index 000000000..27c787b84 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_me_subject.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class GetMeSubject(UncheckedBaseModel): + display_name: str = pydantic.Field() + """ + Human-readable name for the caller. + """ + + id: str = pydantic.Field() + """ + Stable subject identifier for the caller. + """ + + type: str = pydantic.Field() + """ + Subject kind as returned by the identity provider (stored as-is). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_model_provider_catalog_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_model_provider_catalog_response.py new file mode 100644 index 000000000..752ce7eeb --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_model_provider_catalog_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_model_provider import CatalogModelProvider + + +class GetModelProviderCatalogResponse(UncheckedBaseModel): + data: typing.List[CatalogModelProvider] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_model_provider_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_model_provider_response.py new file mode 100644 index 000000000..6384b96a0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_model_provider_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model_provider import ConfiguredModelProvider + + +class GetModelProviderResponse(UncheckedBaseModel): + data: ConfiguredModelProvider + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_provider_catalog_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_provider_catalog_response.py new file mode 100644 index 000000000..77bc66dd4 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_provider_catalog_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_sandbox_provider import CatalogSandboxProvider + + +class GetSandboxProviderCatalogResponse(UncheckedBaseModel): + data: typing.List[CatalogSandboxProvider] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_provider_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_provider_response.py new file mode 100644 index 000000000..fc4dd2a53 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_provider_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_sandbox_provider import ConfiguredSandboxProvider + + +class GetSandboxProviderResponse(UncheckedBaseModel): + data: ConfiguredSandboxProvider + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_schedule_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_schedule_response.py new file mode 100644 index 000000000..0b5bda555 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_schedule_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .schedule import Schedule + + +class GetScheduleResponse(UncheckedBaseModel): + data: Schedule + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_chart_data_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_chart_data_response.py new file mode 100644 index 000000000..cb22d9e7c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_chart_data_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_chart_data_response import SessionMetricsChartDataResponse + + +class GetSessionMetricsChartDataResponse(UncheckedBaseModel): + data: SessionMetricsChartDataResponse + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_chart_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_chart_response.py new file mode 100644 index 000000000..63f5495c7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_chart_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_chart_response import SessionMetricsChartResponse + + +class GetSessionMetricsChartResponse(UncheckedBaseModel): + data: SessionMetricsChartResponse + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_meter_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_meter_response.py new file mode 100644 index 000000000..4c0e880c5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_metrics_meter_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_meter_response import SessionMetricsMeterResponse + + +class GetSessionMetricsMeterResponse(UncheckedBaseModel): + data: SessionMetricsMeterResponse + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_session_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_response.py new file mode 100644 index 000000000..0f3634b76 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_session_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session import Session + + +class GetSessionResponse(UncheckedBaseModel): + data: Session + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_skill_catalog_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_skill_catalog_response.py new file mode 100644 index 000000000..377a11282 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_skill_catalog_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_skill import CatalogSkill + + +class GetSkillCatalogResponse(UncheckedBaseModel): + data: typing.List[CatalogSkill] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_skill_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_skill_response.py new file mode 100644 index 000000000..b16f4c051 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_skill_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_skill import ConfiguredSkill + + +class GetSkillResponse(UncheckedBaseModel): + data: ConfiguredSkill + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_turn_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_turn_response.py new file mode 100644 index 000000000..3e7bd2a6b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_turn_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .turn import Turn + + +class GetTurnResponse(UncheckedBaseModel): + data: Turn + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/git_skill.py b/python/trueforge_sdk/src/trueforge_sdk/types/git_skill.py new file mode 100644 index 000000000..e80b39eff --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/git_skill.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .resource_name import ResourceName + + +class GitSkill(UncheckedBaseModel): + description: str = pydantic.Field() + """ + Concise guidance for when the agent should use the skill. + """ + + name: ResourceName + path: typing.Optional[str] = pydantic.Field(default=None) + """ + Path to the skill directory within the repository. Omit to use the repository root. + """ + + ref: str = pydantic.Field() + """ + Git ref — branch name, tag, or commit SHA. + """ + + type: typing.Literal["git"] = "git" + url: str = pydantic.Field() + """ + Full HTTPS URL of a GitHub or GitLab repository. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/google_gemini_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/google_gemini_model_provider.py new file mode 100644 index 000000000..77e9b8e45 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/google_gemini_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class GoogleGeminiModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://generativelanguage.googleapis.com/v1beta") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["google-gemini"] = "google-gemini" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/initial_user_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/initial_user_message.py new file mode 100644 index 000000000..42cd5a52b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/initial_user_message.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class InitialUserMessage(UncheckedBaseModel): + content: str = pydantic.Field() + """ + Initial user message content injected at the start of every session. + """ + + type: typing.Literal["user.message"] = pydantic.Field(default="user.message") + """ + Initial message type. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/input_tokens_compaction_trigger.py b/python/trueforge_sdk/src/trueforge_sdk/types/input_tokens_compaction_trigger.py new file mode 100644 index 000000000..ccde99a00 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/input_tokens_compaction_trigger.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class InputTokensCompactionTrigger(UncheckedBaseModel): + type: typing.Literal["input_tokens"] = pydantic.Field(default="input_tokens") + """ + Trigger compaction when the estimated input reaches a token limit. + """ + + value: int = pydantic.Field() + """ + Estimated input-token count that triggers compaction. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/large_tool_response_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/large_tool_response_config.py new file mode 100644 index 000000000..3ac829894 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/large_tool_response_config.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class LargeToolResponseConfig(UncheckedBaseModel): + enabled: typing.Optional[bool] = pydantic.Field(default=True) + """ + Offload oversized tool responses to a sandbox file. Default: true. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_agents_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_agents_response.py new file mode 100644 index 000000000..b91c0ba7f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_agents_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent import Agent +from .token_pagination import TokenPagination + + +class ListAgentsResponse(UncheckedBaseModel): + data: typing.List[Agent] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_available_mcp_servers_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_available_mcp_servers_response.py new file mode 100644 index 000000000..4ef79c365 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_available_mcp_servers_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .available_mcp_server import AvailableMcpServer + + +class ListAvailableMcpServersResponse(UncheckedBaseModel): + data: typing.List[AvailableMcpServer] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_available_models_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_available_models_response.py new file mode 100644 index 000000000..bd7a74bc2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_available_models_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .available_model import AvailableModel + + +class ListAvailableModelsResponse(UncheckedBaseModel): + data: typing.List[AvailableModel] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_available_skills_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_available_skills_response.py new file mode 100644 index 000000000..68d0dd77b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_available_skills_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .available_skill import AvailableSkill + + +class ListAvailableSkillsResponse(UncheckedBaseModel): + data: typing.List[AvailableSkill] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_mcp_server_tools_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_mcp_server_tools_response.py new file mode 100644 index 000000000..231ff6f91 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_mcp_server_tools_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ListMcpServerToolsResponse(UncheckedBaseModel): + data: typing.List[typing.Dict[str, typing.Any]] = pydantic.Field() + """ + MCP `tools/list` entries, passed through verbatim from the MCP server. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_mcp_servers_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_mcp_servers_response.py new file mode 100644 index 000000000..2f94a8c3b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_mcp_servers_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_mcp_server import ConfiguredMcpServer + + +class ListMcpServersResponse(UncheckedBaseModel): + data: typing.List[ConfiguredMcpServer] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_model_providers_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_model_providers_response.py new file mode 100644 index 000000000..290608443 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_model_providers_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model_provider import ConfiguredModelProvider + + +class ListModelProvidersResponse(UncheckedBaseModel): + data: typing.List[ConfiguredModelProvider] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_response.py new file mode 100644 index 000000000..8ea35697b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .resource_permission import ResourcePermission + + +class ListPermissionsResponse(UncheckedBaseModel): + data: typing.Dict[str, typing.List[ResourcePermission]] = pydantic.Field() + """ + Permissions granted to the caller, keyed by resource id. Missing or inaccessible ids are `[]`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_schedule_runs_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_schedule_runs_response.py new file mode 100644 index 000000000..2a725cbe7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_schedule_runs_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .schedule_run import ScheduleRun +from .token_pagination import TokenPagination + + +class ListScheduleRunsResponse(UncheckedBaseModel): + data: typing.List[ScheduleRun] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_schedules_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_schedules_response.py new file mode 100644 index 000000000..ab72ca007 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_schedules_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .schedule import Schedule +from .token_pagination import TokenPagination + + +class ListSchedulesResponse(UncheckedBaseModel): + data: typing.List[Schedule] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_session_events_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_session_events_response.py new file mode 100644 index 000000000..feba77e3d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_session_events_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_event_item import SessionEventItem +from .token_pagination import TokenPagination + + +class ListSessionEventsResponse(UncheckedBaseModel): + data: typing.List[SessionEventItem] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_sessions_order.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_sessions_order.py new file mode 100644 index 000000000..07a01f4e9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_sessions_order.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ListSessionsOrder(enum.StrEnum): + """ + Sort sessions by `updated_at`. Defaults to "desc". + """ + + ASC = "asc" + DESC = "desc" + _UNKNOWN = "__LISTSESSIONSORDER_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ListSessionsOrder": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + asc: typing.Callable[[], T_Result], + desc: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ListSessionsOrder.ASC: + return asc() + if self is ListSessionsOrder.DESC: + return desc() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_sessions_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_sessions_response.py new file mode 100644 index 000000000..7c42e0f93 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_sessions_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session import Session +from .token_pagination import TokenPagination + + +class ListSessionsResponse(UncheckedBaseModel): + data: typing.List[Session] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_skill_versions_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_skill_versions_response.py new file mode 100644 index 000000000..87c3be46f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_skill_versions_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .skill_version import SkillVersion + + +class ListSkillVersionsResponse(UncheckedBaseModel): + data: typing.List[SkillVersion] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_skills_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_skills_response.py new file mode 100644 index 000000000..17c09063d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_skills_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_skill import ConfiguredSkill + + +class ListSkillsResponse(UncheckedBaseModel): + data: typing.List[ConfiguredSkill] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_turn_events_order.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_turn_events_order.py new file mode 100644 index 000000000..8adb478ea --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_turn_events_order.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ListTurnEventsOrder(enum.StrEnum): + """ + Sort events by insertion order. Defaults to "asc". + """ + + ASC = "asc" + DESC = "desc" + _UNKNOWN = "__LISTTURNEVENTSORDER_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ListTurnEventsOrder": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + asc: typing.Callable[[], T_Result], + desc: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ListTurnEventsOrder.ASC: + return asc() + if self is ListTurnEventsOrder.DESC: + return desc() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_turn_events_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_turn_events_response.py new file mode 100644 index 000000000..a4fe277c4 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_turn_events_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_event import SessionEvent +from .token_pagination import TokenPagination + + +class ListTurnEventsResponse(UncheckedBaseModel): + data: typing.List[SessionEvent] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_turns_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_turns_response.py new file mode 100644 index 000000000..96783f66f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_turns_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .token_pagination import TokenPagination +from .turn import Turn + + +class ListTurnsResponse(UncheckedBaseModel): + data: typing.List[Turn] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_required_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_required_event.py new file mode 100644 index 000000000..6fca68ed3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_required_event.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from .base_mcp_auth_required_event import BaseMcpAuthRequiredEvent +from .mcp_server_auth_info import McpServerAuthInfo + + +class McpAuthRequiredEvent(BaseMcpAuthRequiredEvent): + mcp_servers: typing.List[McpServerAuthInfo] = pydantic.Field() + """ + Servers that need authorization, each with an auth_url. + """ + + type: typing.Literal["mcp.auth_required"] = "mcp.auth_required" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_status.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_status.py new file mode 100644 index 000000000..1289afc42 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_status.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_auth_status_status import McpAuthStatusStatus + + +class McpAuthStatus(UncheckedBaseModel): + """ + Current auth state. + """ + + authorization_url: typing.Optional[str] = pydantic.Field(default=None) + """ + When auth is required, this contains the URL to redirect the user to for authorization. + """ + + status: McpAuthStatusStatus = pydantic.Field() + """ + Current auth state for this MCP server. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_status_status.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_status_status.py new file mode 100644 index 000000000..4660723d0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_auth_status_status.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class McpAuthStatusStatus(enum.StrEnum): + """ + Current auth state for this MCP server. + """ + + AUTHENTICATED = "authenticated" + AUTH_REQUIRED = "auth_required" + NOT_REQUIRED = "not_required" + _UNKNOWN = "__MCPAUTHSTATUSSTATUS_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "McpAuthStatusStatus": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + authenticated: typing.Callable[[], T_Result], + auth_required: typing.Callable[[], T_Result], + not_required: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is McpAuthStatusStatus.AUTHENTICATED: + return authenticated() + if self is McpAuthStatusStatus.AUTH_REQUIRED: + return auth_required() + if self is McpAuthStatusStatus.NOT_REQUIRED: + return not_required() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_initialize_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_initialize_event.py new file mode 100644 index 000000000..f267baf4e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_initialize_event.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_server_init_info import McpServerInitInfo + + +class McpInitializeEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + mcp_servers: typing.List[McpServerInitInfo] = pydantic.Field() + """ + Servers that were initialized. + """ + + thread_id: str = pydantic.Field() + """ + Thread that triggered initialization. + """ + + type: typing.Literal["mcp.initialize"] = "mcp.initialize" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server.py new file mode 100644 index 000000000..539a3ca52 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_server_approval_tool_selector import McpServerApprovalToolSelector +from .mcp_server_tool_selector import McpServerToolSelector + + +class McpServer(UncheckedBaseModel): + disable_tools: typing.Optional[typing.List[McpServerToolSelector]] = pydantic.Field(default=None) + """ + Tools subtracted from the enabled set. Default: none. + """ + + enable_tools: typing.Optional[typing.List[McpServerToolSelector]] = pydantic.Field(default=None) + """ + Tools exposed to the agent: `@all`, `@read-only`, or literal tool names. Default: `["@all"]`. + """ + + name: str = pydantic.Field() + """ + Name of a configured MCP server (Settings → Connectors). + """ + + preload: typing.Optional[bool] = pydantic.Field(default=False) + """ + When true, load all tool schemas upfront. Default: false (deferred discovery). + """ + + preload_tools: typing.Optional[typing.List[McpServerToolSelector]] = pydantic.Field(default=None) + """ + Tools loaded eagerly into context while the rest stay deferred. A non-empty list implies `preload: false`. + """ + + require_approval_for_tools: typing.Optional[typing.List[McpServerApprovalToolSelector]] = pydantic.Field( + default=None + ) + """ + Tools that pause for human approval: `@all`, `@write`, `@destructive`, or literal names. Default: `["@write", "@destructive"]`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_approval_tool_selector.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_approval_tool_selector.py new file mode 100644 index 000000000..8fa73f111 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_approval_tool_selector.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +McpServerApprovalToolSelector = typing.Union[ + typing.Literal["@all"], typing.Literal["@write"], typing.Literal["@destructive"], str +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_info.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_info.py new file mode 100644 index 000000000..0efcb0dd3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_info.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class McpServerAuthInfo(UncheckedBaseModel): + auth_url: str = pydantic.Field() + """ + URL the user must visit to complete OAuth for this server. + """ + + id: str = pydantic.Field() + """ + Internal MCP server id. + """ + + name: str = pydantic.Field() + """ + Configured MCP server name. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public.py new file mode 100644 index 000000000..3f4d2e719 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .mcp_server_auth_public_one import McpServerAuthPublicOne +from .mcp_server_auth_public_zero import McpServerAuthPublicZero + +McpServerAuthPublic = typing.Union[McpServerAuthPublicZero, McpServerAuthPublicOne] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public_one.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public_one.py new file mode 100644 index 000000000..b8fd1a1c8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public_one.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class McpServerAuthPublicOne(UncheckedBaseModel): + type: typing.Literal["header"] = "header" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public_zero.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public_zero.py new file mode 100644 index 000000000..0b9ff1ed8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_auth_public_zero.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class McpServerAuthPublicZero(UncheckedBaseModel): + type: typing.Literal["dcr"] = "dcr" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_dcr_auth.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_dcr_auth.py new file mode 100644 index 000000000..5b94b8168 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_dcr_auth.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class McpServerDcrAuth(UncheckedBaseModel): + type: typing.Literal["dcr"] = "dcr" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_header_auth.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_header_auth.py new file mode 100644 index 000000000..54596c094 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_header_auth.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class McpServerHeaderAuth(UncheckedBaseModel): + headers: typing.Dict[str, str] = pydantic.Field() + """ + Request headers for this MCP server. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored secret for that header name. + """ + + type: typing.Literal["header"] = "header" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_init_info.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_init_info.py new file mode 100644 index 000000000..74bb464f2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_init_info.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_server_init_info_transport_type import McpServerInitInfoTransportType + + +class McpServerInitInfo(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Internal MCP server id. + """ + + name: str = pydantic.Field() + """ + Configured MCP server name. + """ + + session_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional MCP session id from the transport. + """ + + transport_type: typing.Optional[McpServerInitInfoTransportType] = pydantic.Field(default=None) + """ + Transport used to connect to the MCP server. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_init_info_transport_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_init_info_transport_type.py new file mode 100644 index 000000000..8184a8135 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_init_info_transport_type.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class McpServerInitInfoTransportType(enum.StrEnum): + """ + Transport used to connect to the MCP server. + """ + + STREAMABLE_HTTP = "streamable-http" + SSE = "sse" + _UNKNOWN = "__MCPSERVERINITINFOTRANSPORTTYPE_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "McpServerInitInfoTransportType": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + streamable_http: typing.Callable[[], T_Result], + sse: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is McpServerInitInfoTransportType.STREAMABLE_HTTP: + return streamable_http() + if self is McpServerInitInfoTransportType.SSE: + return sse() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_manifest.py new file mode 100644 index 000000000..e6dd6affa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_manifest.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .remote_mcp_server_manifest import RemoteMcpServerManifest +from .true_foundry_mcp_server_manifest import TrueFoundryMcpServerManifest + +McpServerManifest = typing.Union[RemoteMcpServerManifest, TrueFoundryMcpServerManifest] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_manifest_auth.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_manifest_auth.py new file mode 100644 index 000000000..547199de3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_manifest_auth.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .mcp_server_dcr_auth import McpServerDcrAuth +from .mcp_server_header_auth import McpServerHeaderAuth + +McpServerManifestAuth = typing.Union[McpServerDcrAuth, McpServerHeaderAuth] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_tool_selector.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_tool_selector.py new file mode 100644 index 000000000..66d29937d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_server_tool_selector.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +McpServerToolSelector = typing.Union[typing.Literal["@all"], typing.Literal["@read-only"], str] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/mcp_tool_info.py b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_tool_info.py new file mode 100644 index 000000000..357efa106 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/mcp_tool_info.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class McpToolInfo(UncheckedBaseModel): + name: str = pydantic.Field() + """ + Tool name on the MCP server. + """ + + server_id: str = pydantic.Field() + """ + Internal MCP server id. + """ + + server_name: str = pydantic.Field() + """ + Configured MCP server name. + """ + + type: typing.Literal["mcp"] = "mcp" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/me.py b/python/trueforge_sdk/src/trueforge_sdk/types/me.py new file mode 100644 index 000000000..20bb6c71f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/me.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .get_me_subject import GetMeSubject +from .me_session_type import MeSessionType + + +class Me(UncheckedBaseModel): + roles: typing.List[str] = pydantic.Field() + """ + Roles for the authenticated caller. + """ + + subject: GetMeSubject + tenant_id: str = pydantic.Field() + """ + Tenant scope for the authenticated caller. + """ + + type: MeSessionType + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/me_session_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/me_session_type.py new file mode 100644 index 000000000..ca59a6876 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/me_session_type.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class MeSessionType(enum.StrEnum): + """ + `oidc-connected` when the process is running with browser OIDC login; `default` for standalone or TrueFoundry token auth. + """ + + DEFAULT = "default" + OIDC_CONNECTED = "oidc-connected" + _UNKNOWN = "__MESESSIONTYPE_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "MeSessionType": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + default: typing.Callable[[], T_Result], + oidc_connected: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is MeSessionType.DEFAULT: + return default() + if self is MeSessionType.OIDC_CONNECTED: + return oidc_connected() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/metrics_unit.py b/python/trueforge_sdk/src/trueforge_sdk/types/metrics_unit.py new file mode 100644 index 000000000..b276ef35f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/metrics_unit.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class MetricsUnit(enum.StrEnum): + COUNT = "count" + USD = "$" + MS = "ms" + _UNKNOWN = "__METRICSUNIT_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "MetricsUnit": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + count: typing.Callable[[], T_Result], + usd: typing.Callable[[], T_Result], + ms: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is MetricsUnit.COUNT: + return count() + if self is MetricsUnit.USD: + return usd() + if self is MetricsUnit.MS: + return ms() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model.py b/python/trueforge_sdk/src/trueforge_sdk/types/model.py new file mode 100644 index 000000000..142153cb1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_params import ModelParams + + +class Model(UncheckedBaseModel): + name: str = pydantic.Field() + """ + Model FQN: `provider/model`, e.g. `openai/gpt-5.2`. + """ + + params: typing.Optional[ModelParams] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_message_delta_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_delta_event.py new file mode 100644 index 000000000..be220000e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_delta_event.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .extended_chunk_delta_tool_call import ExtendedChunkDeltaToolCall +from .finish_reason import FinishReason +from .model_message_usage import ModelMessageUsage + + +class ModelMessageDeltaEvent(UncheckedBaseModel): + content: typing.Optional[str] = pydantic.Field(default=None) + """ + Incremental assistant text content. + """ + + created_at: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional ISO 8601 event timestamp. + """ + + finish_reason: typing.Optional[FinishReason] = pydantic.Field(default=None) + """ + Finish reason when this delta completes the stream. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + reasoning_content: typing.Optional[str] = None + refusal: typing.Optional[str] = pydantic.Field(default=None) + """ + Incremental refusal text when present. + """ + + thread_id: str = pydantic.Field() + """ + Thread that emitted this delta. + """ + + tool_calls: typing.Optional[typing.List[ExtendedChunkDeltaToolCall]] = None + type: typing.Literal["model.message.delta"] = "model.message.delta" + usage: typing.Optional[ModelMessageUsage] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event.py new file mode 100644 index 000000000..e3b4157bf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .finish_reason import FinishReason +from .model_message_event_content import ModelMessageEventContent +from .model_message_usage import ModelMessageUsage +from .tool_call import ToolCall + + +class ModelMessageEvent(UncheckedBaseModel): + content: typing.Optional[ModelMessageEventContent] = pydantic.Field(default=None) + """ + Assistant message content as text or content parts. + """ + + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + finish_reason: typing.Optional[FinishReason] = pydantic.Field(default=None) + """ + Model finish reason; null when the provider omitted it. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional participant name. + """ + + reasoning_content: typing.Optional[str] = None + refusal: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional refusal text. + """ + + thread_id: str = pydantic.Field() + """ + Thread that emitted this message (`main` for the root agent). + """ + + tool_calls: typing.Optional[typing.List[ToolCall]] = None + type: typing.Literal["model.message"] = "model.message" + usage: typing.Optional[ModelMessageUsage] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event_content.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event_content.py new file mode 100644 index 000000000..a374ec9d7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event_content.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .model_message_event_content_one_item import ModelMessageEventContentOneItem + +ModelMessageEventContent = typing.Union[str, typing.List[ModelMessageEventContentOneItem]] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event_content_one_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event_content_one_item.py new file mode 100644 index 000000000..9921ddec9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_event_content_one_item.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .chat_completion_content_part_refusal import ChatCompletionContentPartRefusal +from .chat_completion_content_part_text import ChatCompletionContentPartText + +ModelMessageEventContentOneItem = typing.Union[ChatCompletionContentPartText, ChatCompletionContentPartRefusal] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_message_usage.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_usage.py new file mode 100644 index 000000000..3c7b40df9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_usage.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_message_usage_input_tokens_breakdown import ModelMessageUsageInputTokensBreakdown + + +class ModelMessageUsage(UncheckedBaseModel): + cache_read_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Optional cache-read tokens. + """ + + cache_write_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Optional cache-write tokens. + """ + + input_tokens: int = pydantic.Field() + """ + Input tokens for this model call. + """ + + input_tokens_breakdown: ModelMessageUsageInputTokensBreakdown + output_tokens: int = pydantic.Field() + """ + Output tokens for this model call. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_message_usage_input_tokens_breakdown.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_usage_input_tokens_breakdown.py new file mode 100644 index 000000000..17c883f8d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_message_usage_input_tokens_breakdown.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ModelMessageUsageInputTokensBreakdown(UncheckedBaseModel): + harness: int = pydantic.Field() + """ + Tokens attributed to harness system framing. + """ + + instructions: int = pydantic.Field() + """ + Tokens attributed to agent instructions. + """ + + messages: int = pydantic.Field() + """ + Tokens attributed to conversation messages. + """ + + skills: int = pydantic.Field() + """ + Tokens attributed to skill instructions. + """ + + tool_definitions: int = pydantic.Field() + """ + Tokens attributed to tool schemas. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_params.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_params.py new file mode 100644 index 000000000..b0b5a1c98 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_params.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ModelParams(UncheckedBaseModel): + """ + Model call parameters passed through to the provider. Known keys are documented; extra keys are allowed and forwarded as-is. + """ + + max_tokens: typing.Optional[float] = pydantic.Field(default=None) + """ + Maximum tokens to generate in the model response. + """ + + parallel_tool_calls: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether the model may emit multiple tool calls in one response. + """ + + reasoning_effort: typing.Optional[str] = pydantic.Field(default=None) + """ + Provider-specific reasoning effort (e.g. low/medium/high). + """ + + temperature: typing.Optional[float] = pydantic.Field(default=None) + """ + Sampling temperature; higher values increase randomness. + """ + + top_k: typing.Optional[float] = pydantic.Field(default=None) + """ + Top-k sampling; keep only the k highest-probability tokens. + """ + + top_p: typing.Optional[float] = pydantic.Field(default=None) + """ + Nucleus sampling probability mass. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_properties.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_properties.py new file mode 100644 index 000000000..b3175a8e9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_properties.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .reasoning_effort import ReasoningEffort + + +class ModelProperties(UncheckedBaseModel): + """ + Optional model capability metadata. + """ + + context_length: typing.Optional[int] = pydantic.Field(default=None) + """ + Maximum context window size in tokens. + """ + + max_output_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Maximum output tokens the model can generate. + """ + + reasoning_efforts: typing.Optional[typing.List[ReasoningEffort]] = pydantic.Field(default=None) + """ + Supported reasoning-effort values for this model. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_provider_auth.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_provider_auth.py new file mode 100644 index 000000000..166460def --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_provider_auth.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ModelProviderAuth(UncheckedBaseModel): + """ + Provider authentication credentials. + """ + + api_key: str = pydantic.Field() + """ + Provider API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/model_provider_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/model_provider_manifest.py new file mode 100644 index 000000000..abfea71ae --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/model_provider_manifest.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .alibaba_model_provider import AlibabaModelProvider +from .anthropic_model_provider import AnthropicModelProvider +from .custom_model_provider import CustomModelProvider +from .fireworks_model_provider import FireworksModelProvider +from .google_gemini_model_provider import GoogleGeminiModelProvider +from .moonshot_model_provider import MoonshotModelProvider +from .open_ai_model_provider import OpenAiModelProvider +from .together_ai_model_provider import TogetherAiModelProvider +from .true_foundry_model_provider import TrueFoundryModelProvider +from .zai_model_provider import ZaiModelProvider + +ModelProviderManifest = typing.Union[ + AlibabaModelProvider, + AnthropicModelProvider, + CustomModelProvider, + FireworksModelProvider, + GoogleGeminiModelProvider, + MoonshotModelProvider, + OpenAiModelProvider, + TogetherAiModelProvider, + TrueFoundryModelProvider, + ZaiModelProvider, +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/moonshot_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/moonshot_model_provider.py new file mode 100644 index 000000000..2f5b61ccd --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/moonshot_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class MoonshotModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://api.moonshot.ai/v1") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["moonshot"] = "moonshot" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/open_ai_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/open_ai_model_provider.py new file mode 100644 index 000000000..b9d2d08aa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/open_ai_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class OpenAiModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://api.openai.com/v1") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["openai"] = "openai" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/permission_resource_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/permission_resource_type.py new file mode 100644 index 000000000..59ca69be1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/permission_resource_type.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class PermissionResourceType(enum.StrEnum): + """ + Resource type to evaluate permissions for. + """ + + AGENT = "agent" + SCHEDULE = "schedule" + SESSION = "session" + _UNKNOWN = "__PERMISSIONRESOURCETYPE_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "PermissionResourceType": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + agent: typing.Callable[[], T_Result], + schedule: typing.Callable[[], T_Result], + session: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is PermissionResourceType.AGENT: + return agent() + if self is PermissionResourceType.SCHEDULE: + return schedule() + if self is PermissionResourceType.SESSION: + return session() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/previous_turn_id_input.py b/python/trueforge_sdk/src/trueforge_sdk/types/previous_turn_id_input.py new file mode 100644 index 000000000..0e5f035f9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/previous_turn_id_input.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +PreviousTurnIdInput = typing.Union[typing.Literal["auto"], typing.Literal["none"], str] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/raw_tool_call.py b/python/trueforge_sdk/src/trueforge_sdk/types/raw_tool_call.py new file mode 100644 index 000000000..baed6a381 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/raw_tool_call.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from .chat_completion_message_tool_call import ChatCompletionMessageToolCall + + +class RawToolCall(ChatCompletionMessageToolCall): + provider_specific_fields: typing.Optional[typing.Dict[str, typing.Any]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/reasoning_effort.py b/python/trueforge_sdk/src/trueforge_sdk/types/reasoning_effort.py new file mode 100644 index 000000000..4b71d9b98 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/reasoning_effort.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ReasoningEffort(enum.StrEnum): + NONE = "none" + MINIMAL = "minimal" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + XHIGH = "xhigh" + MAX = "max" + _UNKNOWN = "__REASONINGEFFORT_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ReasoningEffort": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + none: typing.Callable[[], T_Result], + minimal: typing.Callable[[], T_Result], + low: typing.Callable[[], T_Result], + medium: typing.Callable[[], T_Result], + high: typing.Callable[[], T_Result], + xhigh: typing.Callable[[], T_Result], + max: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ReasoningEffort.NONE: + return none() + if self is ReasoningEffort.MINIMAL: + return minimal() + if self is ReasoningEffort.LOW: + return low() + if self is ReasoningEffort.MEDIUM: + return medium() + if self is ReasoningEffort.HIGH: + return high() + if self is ReasoningEffort.XHIGH: + return xhigh() + if self is ReasoningEffort.MAX: + return max() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/remote_mcp_server_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/remote_mcp_server_manifest.py new file mode 100644 index 000000000..185f3eb61 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/remote_mcp_server_manifest.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_server_manifest_auth import McpServerManifestAuth +from .resource_name import ResourceName + + +class RemoteMcpServerManifest(UncheckedBaseModel): + auth: typing.Optional[McpServerManifestAuth] = None + description: str = pydantic.Field() + """ + Concise summary of what this MCP server provides. + """ + + name: ResourceName + type: typing.Literal["remote"] = "remote" + url: str = pydantic.Field() + """ + MCP endpoint URL. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/request_error_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/request_error_response.py new file mode 100644 index 000000000..a720ca592 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/request_error_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .request_error_response_error import RequestErrorResponseError + + +class RequestErrorResponse(UncheckedBaseModel): + error: RequestErrorResponseError + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/request_error_response_error.py b/python/trueforge_sdk/src/trueforge_sdk/types/request_error_response_error.py new file mode 100644 index 000000000..760a84d23 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/request_error_response_error.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class RequestErrorResponseError(UncheckedBaseModel): + code: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional machine-readable error code; null when not applicable. + """ + + message: str = pydantic.Field() + """ + Human-readable explanation of the failure. + """ + + param: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional request field that caused the error; null when not field-specific. + """ + + type: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional error category (e.g. validation vs conflict). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/resource_name.py b/python/trueforge_sdk/src/trueforge_sdk/types/resource_name.py new file mode 100644 index 000000000..4537b61f9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/resource_name.py @@ -0,0 +1,3 @@ +# This file was auto-generated by Fern from our API Definition. + +ResourceName = str diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/resource_permission.py b/python/trueforge_sdk/src/trueforge_sdk/types/resource_permission.py new file mode 100644 index 000000000..a26c3601e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/resource_permission.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ResourcePermission(enum.StrEnum): + """ + Granted action on a resource. + """ + + USE = "USE" + MANAGE = "MANAGE" + DELETE = "DELETE" + _UNKNOWN = "__RESOURCEPERMISSION_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ResourcePermission": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + use: typing.Callable[[], T_Result], + manage: typing.Callable[[], T_Result], + delete: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ResourcePermission.USE: + return use() + if self is ResourcePermission.MANAGE: + return manage() + if self is ResourcePermission.DELETE: + return delete() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/response_format.py b/python/trueforge_sdk/src/trueforge_sdk/types/response_format.py new file mode 100644 index 000000000..6f7b373d8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/response_format.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .response_format_json_object import ResponseFormatJsonObject +from .response_format_json_schema import ResponseFormatJsonSchema +from .response_format_text import ResponseFormatText + +ResponseFormat = typing.Union[ResponseFormatJsonObject, ResponseFormatJsonSchema, ResponseFormatText] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_object.py b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_object.py new file mode 100644 index 000000000..b04736c2a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_object.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ResponseFormatJsonObject(UncheckedBaseModel): + """ + JSON object response format. Extra provider fields are allowed. + """ + + type: typing.Literal["json_object"] = "json_object" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_schema.py b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_schema.py new file mode 100644 index 000000000..24a2147fc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_schema.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .response_format_json_schema_json_schema import ResponseFormatJsonSchemaJsonSchema + + +class ResponseFormatJsonSchema(UncheckedBaseModel): + """ + JSON Schema response format. Extra provider fields are allowed. + """ + + json_schema: ResponseFormatJsonSchemaJsonSchema = pydantic.Field() + """ + JSON Schema payload. Extra provider fields are allowed. + """ + + type: typing.Literal["json_schema"] = "json_schema" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_schema_json_schema.py b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_schema_json_schema.py new file mode 100644 index 000000000..bd1f6ec4b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_json_schema_json_schema.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ResponseFormatJsonSchemaJsonSchema(UncheckedBaseModel): + """ + JSON Schema payload. Extra provider fields are allowed. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional schema description for the model. + """ + + name: str = pydantic.Field() + """ + Schema name sent to the provider. + """ + + schema_: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Any]], + FieldMetadata(alias="schema"), + pydantic.Field(alias="schema", description="JSON Schema object for the response."), + ] = None + """ + JSON Schema object for the response. + """ + + strict: typing.Optional[bool] = pydantic.Field(default=None) + """ + When true, ask the provider to enforce the schema strictly. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/response_format_text.py b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_text.py new file mode 100644 index 000000000..0461a9f35 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/response_format_text.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ResponseFormatText(UncheckedBaseModel): + """ + Default text response format. Extra provider fields are allowed. + """ + + type: typing.Literal["text"] = "text" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/runtime_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/runtime_config.py new file mode 100644 index 000000000..322242491 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/runtime_config.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .ask_user_questions_config import AskUserQuestionsConfig +from .context_management_config import ContextManagementConfig +from .dynamic_sub_agents_config import DynamicSubAgentsConfig +from .generative_ui_config import GenerativeUiConfig +from .sandbox_config import SandboxConfig + + +class RuntimeConfig(UncheckedBaseModel): + ask_user_questions: typing.Optional[AskUserQuestionsConfig] = None + context_management: typing.Optional[ContextManagementConfig] = None + dynamic_sub_agents: typing.Optional[DynamicSubAgentsConfig] = None + generative_ui: typing.Optional[GenerativeUiConfig] = None + iteration_limit: typing.Optional[int] = pydantic.Field(default=100) + """ + Max agent-loop iterations per turn (1–1024). Default: 100. + """ + + sandbox: typing.Optional[SandboxConfig] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_build_status.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_build_status.py new file mode 100644 index 000000000..1f7627450 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_build_status.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class SandboxBuildStatus(enum.StrEnum): + """ + Current build status. + """ + + PENDING = "pending" + READY = "ready" + FAILED = "failed" + _UNKNOWN = "__SANDBOXBUILDSTATUS_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "SandboxBuildStatus": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + pending: typing.Callable[[], T_Result], + ready: typing.Callable[[], T_Result], + failed: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is SandboxBuildStatus.PENDING: + return pending() + if self is SandboxBuildStatus.READY: + return ready() + if self is SandboxBuildStatus.FAILED: + return failed() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_capability.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_capability.py new file mode 100644 index 000000000..3e663c8f2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_capability.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SandboxCapability(UncheckedBaseModel): + enabled: bool = pydantic.Field() + """ + Whether a sandbox provider is configured for this tenant. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py new file mode 100644 index 000000000..d3e7b3ae5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SandboxConfig(UncheckedBaseModel): + enabled: bool = pydantic.Field() + """ + Give the agent a sandbox. Required for skills and Code Mode. + """ + + file_downloads: typing.Optional[bool] = pydantic.Field(default=True) + """ + Allow downloading agent-produced files via the turn download endpoint. Default: true. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_created_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_created_event.py new file mode 100644 index 000000000..c7217ff30 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_created_event.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SandboxCreatedEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + sandbox_id: str = pydantic.Field() + """ + Provider sandbox id. + """ + + thread_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Always null — sandbox is session-scoped. + """ + + type: typing.Literal["sandbox.created"] = "sandbox.created" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_provider_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_provider_manifest.py new file mode 100644 index 000000000..d6f363b82 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_provider_manifest.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .daytona_sandbox_provider_auth import DaytonaSandboxProviderAuth + + +class SandboxProviderManifest(UncheckedBaseModel): + auth: DaytonaSandboxProviderAuth + auto_archive_interval_in_minutes: int = pydantic.Field() + """ + Minutes before Daytona auto-archives the sandbox (0 disables). + """ + + auto_delete_interval_in_minutes: int = pydantic.Field() + """ + Minutes before Daytona auto-deletes the sandbox (0 disables). + """ + + auto_stop_interval_in_minutes: int = pydantic.Field() + """ + Minutes of idle time before Daytona auto-stops the sandbox (0 disables). + """ + + exec_timeout_ms: int = pydantic.Field() + """ + Default sandbox command exec timeout in milliseconds. + """ + + type: typing.Literal["daytona"] = pydantic.Field(default="daytona") + """ + Daytona sandbox provider. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/schedule.py b/python/trueforge_sdk/src/trueforge_sdk/types/schedule.py new file mode 100644 index 000000000..4deb9fa7b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/schedule.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .created_by_subject import CreatedBySubject +from .resource_name import ResourceName +from .schedule_manifest import ScheduleManifest + + +class Schedule(UncheckedBaseModel): + agent_name: ResourceName + created_at: dt.datetime + created_by_subject: CreatedBySubject + id: str + manifest: ScheduleManifest + name: ResourceName + updated_at: dt.datetime + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/schedule_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_manifest.py new file mode 100644 index 000000000..e3849885d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_manifest.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .cron_expression import CronExpression +from .schedule_status import ScheduleStatus +from .timezone import Timezone + + +class ScheduleManifest(UncheckedBaseModel): + cron: CronExpression + status: typing.Optional[ScheduleStatus] = None + task: str = pydantic.Field() + """ + First user message sent to the agent on every run. + """ + + timezone: typing.Optional[Timezone] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/schedule_run.py b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_run.py new file mode 100644 index 000000000..9baeaf1cf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_run.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .created_by_subject import CreatedBySubject +from .schedule_run_status import ScheduleRunStatus + + +class ScheduleRun(UncheckedBaseModel): + created_at: dt.datetime + created_by_subject: CreatedBySubject + id: str + name: str + reason: typing.Optional[str] = None + schedule_id: str + scheduled_for: dt.datetime + status: ScheduleRunStatus + triggered_at: typing.Optional[dt.datetime] = None + updated_at: dt.datetime + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/schedule_run_status.py b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_run_status.py new file mode 100644 index 000000000..64d1a1d7a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_run_status.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ScheduleRunStatus(enum.StrEnum): + SCHEDULED = "scheduled" + TRIGGERED = "triggered" + FAILED = "failed" + _UNKNOWN = "__SCHEDULERUNSTATUS_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ScheduleRunStatus": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + scheduled: typing.Callable[[], T_Result], + triggered: typing.Callable[[], T_Result], + failed: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ScheduleRunStatus.SCHEDULED: + return scheduled() + if self is ScheduleRunStatus.TRIGGERED: + return triggered() + if self is ScheduleRunStatus.FAILED: + return failed() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/schedule_status.py b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_status.py new file mode 100644 index 000000000..6d67b63bc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/schedule_status.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ScheduleStatus(enum.StrEnum): + ACTIVE = "active" + PAUSED = "paused" + _UNKNOWN = "__SCHEDULESTATUS_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ScheduleStatus": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + active: typing.Callable[[], T_Result], + paused: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ScheduleStatus.ACTIVE: + return active() + if self is ScheduleStatus.PAUSED: + return paused() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session.py b/python/trueforge_sdk/src/trueforge_sdk/types/session.py new file mode 100644 index 000000000..69b629dc6 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .created_by_subject import CreatedBySubject +from .session_agent import SessionAgent +from .session_metadata import SessionMetadata +from .session_metrics import SessionMetrics +from .session_source import SessionSource + + +class Session(UncheckedBaseModel): + agent: SessionAgent + created_at: str = pydantic.Field() + """ + ISO 8601 creation timestamp. + """ + + created_by_subject: CreatedBySubject + id: str = pydantic.Field() + """ + Unique session id. + """ + + metadata: SessionMetadata + metrics: SessionMetrics + source: typing.Optional[SessionSource] = None + title: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional human-readable title; null until set. + """ + + updated_at: str = pydantic.Field() + """ + ISO 8601 last-update timestamp. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_agent.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent.py new file mode 100644 index 000000000..86c1ee102 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .session_agent_inline import SessionAgentInline +from .session_agent_reference import SessionAgentReference + +SessionAgent = typing.Union[SessionAgentInline, SessionAgentReference] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_inline.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_inline.py new file mode 100644 index 000000000..4e3b65131 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_inline.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_spec import AgentSpec + + +class SessionAgentInline(UncheckedBaseModel): + spec: AgentSpec + type: typing.Literal["inline"] = "inline" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_name_ref.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_name_ref.py new file mode 100644 index 000000000..dfda31371 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_name_ref.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .resource_name import ResourceName + + +class SessionAgentNameRef(UncheckedBaseModel): + name: ResourceName + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_reference.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_reference.py new file mode 100644 index 000000000..9f397db0e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_reference.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionAgentReference(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Registry agent id. + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Create-time snapshot of the registry agent name; null for legacy or orphan rows. + """ + + type: typing.Literal["reference"] = "reference" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_spec_body.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_spec_body.py new file mode 100644 index 000000000..df612002e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_agent_spec_body.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_spec import AgentSpec + + +class SessionAgentSpecBody(UncheckedBaseModel): + spec: AgentSpec + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py new file mode 100644 index 000000000..94f737c15 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .mcp_auth_required_event import McpAuthRequiredEvent +from .mcp_initialize_event import McpInitializeEvent +from .model_message_event import ModelMessageEvent +from .sandbox_created_event import SandboxCreatedEvent +from .thread_created_event import ThreadCreatedEvent +from .thread_done_event import ThreadDoneEvent +from .tool_approval_required_event import ToolApprovalRequiredEvent +from .tool_response_event import ToolResponseEvent +from .tool_response_required_event import ToolResponseRequiredEvent +from .turn_created_event import TurnCreatedEvent +from .turn_done_event import TurnDoneEvent + +SessionEvent = typing.Union[ + McpAuthRequiredEvent, + McpInitializeEvent, + ModelMessageEvent, + SandboxCreatedEvent, + ThreadCreatedEvent, + ThreadDoneEvent, + ToolApprovalRequiredEvent, + ToolResponseEvent, + ToolResponseRequiredEvent, + TurnCreatedEvent, + TurnDoneEvent, +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_event_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_event_item.py new file mode 100644 index 000000000..6ce90907a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_event_item.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_event import SessionEvent + + +class SessionEventItem(UncheckedBaseModel): + event: SessionEvent + turn_id: str = pydantic.Field() + """ + Turn that emitted this event. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metadata.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metadata.py new file mode 100644 index 000000000..260fbb0fd --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metadata.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SessionMetadata = typing.Dict[str, str] +""" +Caller-owned session metadata +""" diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics.py new file mode 100644 index 000000000..39e772df0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionMetrics(UncheckedBaseModel): + """ + Rolled-up cost, duration, and turn counters for a session. + """ + + total_cost_in_usd: typing.Optional[float] = pydantic.Field(default=None) + """ + Estimated total cost in USD for this session + """ + + total_duration_ms: int + total_turns: int + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart.py new file mode 100644 index 000000000..e817decc5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_chart_name import SessionMetricsChartName + + +class SessionMetricsChart(UncheckedBaseModel): + chart_type: typing.Literal["line"] = "line" + description: str + display_name: str + name: SessionMetricsChartName + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_data_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_data_response.py new file mode 100644 index 000000000..f0be8de0c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_data_response.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_graph import SessionMetricsGraph + + +class SessionMetricsChartDataResponse(UncheckedBaseModel): + graphs: typing.List[SessionMetricsGraph] + step: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_name.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_name.py new file mode 100644 index 000000000..92c4ab43b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_name.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class SessionMetricsChartName(enum.StrEnum): + """ + Session metrics chart to return. + """ + + SESSIONS_OVER_TIME = "sessions_over_time" + SESSIONS_COST_OVER_TIME = "sessions_cost_over_time" + TURNS_OVER_TIME = "turns_over_time" + _UNKNOWN = "__SESSIONMETRICSCHARTNAME_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "SessionMetricsChartName": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + sessions_over_time: typing.Callable[[], T_Result], + sessions_cost_over_time: typing.Callable[[], T_Result], + turns_over_time: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is SessionMetricsChartName.SESSIONS_OVER_TIME: + return sessions_over_time() + if self is SessionMetricsChartName.SESSIONS_COST_OVER_TIME: + return sessions_cost_over_time() + if self is SessionMetricsChartName.TURNS_OVER_TIME: + return turns_over_time() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_response.py new file mode 100644 index 000000000..92594d14b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_chart_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_chart import SessionMetricsChart + + +class SessionMetricsChartResponse(UncheckedBaseModel): + charts: typing.List[SessionMetricsChart] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_graph.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_graph.py new file mode 100644 index 000000000..4dc219e15 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_graph.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .metrics_unit import MetricsUnit +from .session_metrics_chart_name import SessionMetricsChartName +from .session_metrics_graph_line import SessionMetricsGraphLine + + +class SessionMetricsGraph(UncheckedBaseModel): + chart_type: typing.Literal["line"] = "line" + description: str + display_name: str + graph_lines: typing.List[SessionMetricsGraphLine] + name: SessionMetricsChartName + unit: MetricsUnit + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_graph_line.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_graph_line.py new file mode 100644 index 000000000..d839a8dfe --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_graph_line.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_point import SessionMetricsPoint + + +class SessionMetricsGraphLine(UncheckedBaseModel): + name: str + values: typing.List[SessionMetricsPoint] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter.py new file mode 100644 index 000000000..fc4d649ad --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .metrics_unit import MetricsUnit +from .session_metrics_meter_name import SessionMetricsMeterName + + +class SessionMetricsMeter(UncheckedBaseModel): + aggregate_value: float + description: str + name: SessionMetricsMeterName + unit: MetricsUnit + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter_name.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter_name.py new file mode 100644 index 000000000..d59bd5d03 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter_name.py @@ -0,0 +1,74 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class SessionMetricsMeterName(enum.StrEnum): + TOTAL_SESSIONS = "total_sessions" + TOTAL_COST_IN_USD = "total_cost_in_usd" + TOTAL_TURNS = "total_turns" + COST_PER_SESSION_IN_USD = "cost_per_session_in_usd" + AVG_TURNS_PER_SESSION = "avg_turns_per_session" + MIN_TURNS_PER_SESSION = "min_turns_per_session" + MAX_TURNS_PER_SESSION = "max_turns_per_session" + MEDIAN_TURNS_PER_SESSION = "median_turns_per_session" + MIN_SESSION_DURATION_MS = "min_session_duration_ms" + MAX_SESSION_DURATION_MS = "max_session_duration_ms" + MEDIAN_SESSION_DURATION_MS = "median_session_duration_ms" + P95SESSION_DURATION_MS = "p95_session_duration_ms" + _UNKNOWN = "__SESSIONMETRICSMETERNAME_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "SessionMetricsMeterName": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + total_sessions: typing.Callable[[], T_Result], + total_cost_in_usd: typing.Callable[[], T_Result], + total_turns: typing.Callable[[], T_Result], + cost_per_session_in_usd: typing.Callable[[], T_Result], + avg_turns_per_session: typing.Callable[[], T_Result], + min_turns_per_session: typing.Callable[[], T_Result], + max_turns_per_session: typing.Callable[[], T_Result], + median_turns_per_session: typing.Callable[[], T_Result], + min_session_duration_ms: typing.Callable[[], T_Result], + max_session_duration_ms: typing.Callable[[], T_Result], + median_session_duration_ms: typing.Callable[[], T_Result], + p95session_duration_ms: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is SessionMetricsMeterName.TOTAL_SESSIONS: + return total_sessions() + if self is SessionMetricsMeterName.TOTAL_COST_IN_USD: + return total_cost_in_usd() + if self is SessionMetricsMeterName.TOTAL_TURNS: + return total_turns() + if self is SessionMetricsMeterName.COST_PER_SESSION_IN_USD: + return cost_per_session_in_usd() + if self is SessionMetricsMeterName.AVG_TURNS_PER_SESSION: + return avg_turns_per_session() + if self is SessionMetricsMeterName.MIN_TURNS_PER_SESSION: + return min_turns_per_session() + if self is SessionMetricsMeterName.MAX_TURNS_PER_SESSION: + return max_turns_per_session() + if self is SessionMetricsMeterName.MEDIAN_TURNS_PER_SESSION: + return median_turns_per_session() + if self is SessionMetricsMeterName.MIN_SESSION_DURATION_MS: + return min_session_duration_ms() + if self is SessionMetricsMeterName.MAX_SESSION_DURATION_MS: + return max_session_duration_ms() + if self is SessionMetricsMeterName.MEDIAN_SESSION_DURATION_MS: + return median_session_duration_ms() + if self is SessionMetricsMeterName.P95SESSION_DURATION_MS: + return p95session_duration_ms() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter_response.py new file mode 100644 index 000000000..66b637007 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_meter_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_metrics_meter import SessionMetricsMeter + + +class SessionMetricsMeterResponse(UncheckedBaseModel): + meters: typing.List[SessionMetricsMeter] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_point.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_point.py new file mode 100644 index 000000000..4f11f9fd5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_metrics_point.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionMetricsPoint(UncheckedBaseModel): + timestamp: str + value: float + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_source.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_source.py new file mode 100644 index 000000000..dda2353c9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_source.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionSource(UncheckedBaseModel): + """ + How this session was created (e.g. a schedule run). Null for interactive sessions. + """ + + id: str = pydantic.Field() + """ + Schedule id. + """ + + run_id: str = pydantic.Field() + """ + Schedule run id. + """ + + type: typing.Literal["schedule"] = pydantic.Field(default="schedule") + """ + Session was created by a schedule run. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_source_schedule.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_source_schedule.py new file mode 100644 index 000000000..c8b4b4f2f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_source_schedule.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionSourceSchedule(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Schedule id. + """ + + run_id: str = pydantic.Field() + """ + Schedule run id. + """ + + type: typing.Literal["schedule"] = pydantic.Field(default="schedule") + """ + Session was created by a schedule run. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_source_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_source_type.py new file mode 100644 index 000000000..d82e7f67a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_source_type.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SessionSourceType = typing.Literal["schedule"] +""" +When set, returns only sessions created by this source type. +""" diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/settings_capability.py b/python/trueforge_sdk/src/trueforge_sdk/types/settings_capability.py new file mode 100644 index 000000000..dff3e54e2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/settings_capability.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SettingsCapability(UncheckedBaseModel): + enabled: bool = pydantic.Field() + """ + Whether the settings UI/API is enabled. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/skill.py b/python/trueforge_sdk/src/trueforge_sdk/types/skill.py new file mode 100644 index 000000000..833890d4c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/skill.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class Skill(UncheckedBaseModel): + name: str = pydantic.Field() + """ + Skill name. + """ + + preload: typing.Optional[bool] = pydantic.Field(default=False) + """ + Inline SKILL.md into the prompt. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/skill_capability.py b/python/trueforge_sdk/src/trueforge_sdk/types/skill_capability.py new file mode 100644 index 000000000..d6dd4f939 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/skill_capability.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SkillCapability(UncheckedBaseModel): + enabled: bool = pydantic.Field() + """ + Whether skills are available. False when sandbox is not enabled (skills require a sandbox). + """ + + reason: typing.Optional[str] = pydantic.Field(default=None) + """ + Present when skills are disabled. Explains why. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/skill_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/skill_manifest.py new file mode 100644 index 000000000..cb8680e87 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/skill_manifest.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .git_skill import GitSkill +from .true_foundry_registry_skill import TrueFoundryRegistrySkill + +SkillManifest = typing.Union[GitSkill, TrueFoundryRegistrySkill] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/skill_version.py b/python/trueforge_sdk/src/trueforge_sdk/types/skill_version.py new file mode 100644 index 000000000..f9136e768 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/skill_version.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SkillVersion(UncheckedBaseModel): + description: str = pydantic.Field() + """ + Concise guidance for when the agent should use the skill. + """ + + display_name: str + name: str + version: int + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/text_content.py b/python/trueforge_sdk/src/trueforge_sdk/types/text_content.py new file mode 100644 index 000000000..6893a0e98 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/text_content.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TextContent(UncheckedBaseModel): + text: str = pydantic.Field() + """ + Plain-text content. + """ + + type: typing.Literal["text"] = "text" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/thread_created_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/thread_created_event.py new file mode 100644 index 000000000..3ca2750bf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/thread_created_event.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .agent_info import AgentInfo +from .agent_parent import AgentParent + + +class ThreadCreatedEvent(UncheckedBaseModel): + agent_info: AgentInfo + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + parent: AgentParent + thread_id: str = pydantic.Field() + """ + Id of the new thread. + """ + + title: str = pydantic.Field() + """ + Human-readable thread title. + """ + + type: typing.Literal["thread.created"] = "thread.created" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/thread_done_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/thread_done_event.py new file mode 100644 index 000000000..c1857c1c9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/thread_done_event.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from .base_thread_done_event import BaseThreadDoneEvent +from .thread_state import ThreadState + + +class ThreadDoneEvent(BaseThreadDoneEvent): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + state: ThreadState + type: typing.Literal["thread.done"] = "thread.done" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/thread_state.py b/python/trueforge_sdk/src/trueforge_sdk/types/thread_state.py new file mode 100644 index 000000000..d69da3db5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/thread_state.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .thread_state_done import ThreadStateDone +from .thread_state_error import ThreadStateError + +ThreadState = typing.Union[ThreadStateDone, ThreadStateError] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/thread_state_done.py b/python/trueforge_sdk/src/trueforge_sdk/types/thread_state_done.py new file mode 100644 index 000000000..912c2bb7e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/thread_state_done.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_message_event import ModelMessageEvent + + +class ThreadStateDone(UncheckedBaseModel): + output: ModelMessageEvent + status: typing.Literal["done"] = "done" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/thread_state_error.py b/python/trueforge_sdk/src/trueforge_sdk/types/thread_state_error.py new file mode 100644 index 000000000..7ce8b1250 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/thread_state_error.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .model_message_event import ModelMessageEvent + + +class ThreadStateError(UncheckedBaseModel): + error: str = pydantic.Field() + """ + Human-readable error message. + """ + + output: typing.Optional[ModelMessageEvent] = None + status: typing.Literal["error"] = "error" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/timezone.py b/python/trueforge_sdk/src/trueforge_sdk/types/timezone.py new file mode 100644 index 000000000..28b517a60 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/timezone.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +Timezone = str +""" +IANA time zone the cron expression is evaluated in. +""" diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/together_ai_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/together_ai_model_provider.py new file mode 100644 index 000000000..5c22a45fa --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/together_ai_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class TogetherAiModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://api.together.xyz/v1") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["together"] = "together" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/token_pagination.py b/python/trueforge_sdk/src/trueforge_sdk/types/token_pagination.py new file mode 100644 index 000000000..a80563a4b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/token_pagination.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TokenPagination(UncheckedBaseModel): + limit: int = pydantic.Field() + """ + Page size used for this response. + """ + + next_page_token: typing.Optional[str] = pydantic.Field(default=None) + """ + Opaque token for the next page. Omit or absent when there is no next page. + """ + + previous_page_token: typing.Optional[str] = pydantic.Field(default=None) + """ + Opaque token for the previous page. Omit or absent when there is no previous page. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_required_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_required_event.py new file mode 100644 index 000000000..cf1cbaa22 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_required_event.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_call_ref import ToolCallRef + + +class ToolApprovalRequiredEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool calls. + """ + + tool_calls: typing.List[ToolCallRef] = pydantic.Field() + """ + Tool calls waiting for approval. + """ + + type: typing.Literal["tool.approval_required"] = "tool.approval_required" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_call.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_call.py new file mode 100644 index 000000000..29ffe6400 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_call.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from .raw_tool_call import RawToolCall +from .tool_info import ToolInfo + + +class ToolCall(RawToolCall): + tool_info: ToolInfo + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_call_ref.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_call_ref.py new file mode 100644 index 000000000..6f99d511b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_call_ref.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ToolCallRef(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Tool call id awaiting action. + """ + + source_event_id: str = pydantic.Field() + """ + Event id of the model.message that requested the tool call. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_info.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_info.py new file mode 100644 index 000000000..8e9c15f38 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_info.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .mcp_tool_info import McpToolInfo +from .true_foundry_system_tool_info import TrueFoundrySystemToolInfo + +ToolInfo = typing.Union[McpToolInfo, TrueFoundrySystemToolInfo] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_response_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_response_event.py new file mode 100644 index 000000000..8f729dca7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_response_event.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ToolResponseEvent(UncheckedBaseModel): + content: str + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Id of the tool call this message responds to. + """ + + type: typing.Literal["tool.response"] = "tool.response" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_response_required_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_response_required_event.py new file mode 100644 index 000000000..615038aee --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_response_required_event.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_call_ref import ToolCallRef + + +class ToolResponseRequiredEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool calls. + """ + + tool_calls: typing.List[ToolCallRef] = pydantic.Field() + """ + Tool calls waiting for a client response. + """ + + type: typing.Literal["tool.response_required"] = "tool.response_required" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_mcp_server_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_mcp_server_manifest.py new file mode 100644 index 000000000..675f83503 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_mcp_server_manifest.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .mcp_server_manifest_auth import McpServerManifestAuth +from .resource_name import ResourceName + + +class TrueFoundryMcpServerManifest(UncheckedBaseModel): + auth: typing.Optional[McpServerManifestAuth] = None + description: str = pydantic.Field() + """ + Concise summary of what this MCP server provides. + """ + + name: ResourceName + type: typing.Literal["truefoundry"] = "truefoundry" + url: str = pydantic.Field() + """ + Resolved AI Gateway proxy URL for this TrueFoundry-managed MCP server. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_model_provider.py new file mode 100644 index 000000000..763c50c7f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class TrueFoundryModelProvider(UncheckedBaseModel): + auth: typing.Optional[ModelProviderAuth] = None + base_url: str = pydantic.Field() + """ + Base URL of the TrueFoundry AI gateway the models are invoked against. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["truefoundry"] = "truefoundry" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_registry_skill.py b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_registry_skill.py new file mode 100644 index 000000000..ac6faef20 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_registry_skill.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TrueFoundryRegistrySkill(UncheckedBaseModel): + description: str = pydantic.Field() + """ + Concise guidance for when the agent should use the skill. + """ + + display_name: str + name: str + repository_name: str = pydantic.Field() + """ + Repo where the skill is registered. + """ + + type: typing.Literal["truefoundry"] = "truefoundry" + version: int + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_system_tool_info.py b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_system_tool_info.py new file mode 100644 index 000000000..3115d70b2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/true_foundry_system_tool_info.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TrueFoundrySystemToolInfo(UncheckedBaseModel): + name: str = pydantic.Field() + """ + System tool name. + """ + + type: typing.Literal["truefoundry-system"] = "truefoundry-system" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn.py new file mode 100644 index 000000000..7ef2e05ee --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .turn_input_item import TurnInputItem +from .turn_state import TurnState + + +class Turn(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 creation timestamp. + """ + + id: str = pydantic.Field() + """ + Unique turn id. + """ + + input: typing.Optional[typing.List[TurnInputItem]] = pydantic.Field(default=None) + """ + Input items supplied when the turn was created. + """ + + previous_turn_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Prior turn this turn chains from; null for a root turn. + """ + + session_id: str = pydantic.Field() + """ + Session that owns this turn. + """ + + state: TurnState + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_created_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_created_event.py new file mode 100644 index 000000000..89283a321 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_created_event.py @@ -0,0 +1,52 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .turn_input_item import TurnInputItem +from .turn_state_running import TurnStateRunning + + +class TurnCreatedEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + input: typing.Optional[typing.List[TurnInputItem]] = pydantic.Field(default=None) + """ + Input items supplied when the turn was created. + """ + + previous_turn_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Prior turn this turn chains from; null for a root turn. + """ + + state: TurnStateRunning + thread_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Thread that owns the event; null for turn-level lifecycle events. + """ + + turn_id: str = pydantic.Field() + """ + Id of the newly created turn. + """ + + type: typing.Literal["turn.created"] = "turn.created" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_done_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_done_event.py new file mode 100644 index 000000000..8683bee14 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_done_event.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .turn_done_event_state import TurnDoneEventState + + +class TurnDoneEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + state: TurnDoneEventState = pydantic.Field() + """ + Terminal turn state (done, cancelled, or error). + """ + + thread_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Thread that owns the event; null for turn-level lifecycle events. + """ + + type: typing.Literal["turn.done"] = "turn.done" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_done_event_state.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_done_event_state.py new file mode 100644 index 000000000..4518451dc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_done_event_state.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .turn_state_cancelled import TurnStateCancelled +from .turn_state_done import TurnStateDone +from .turn_state_error import TurnStateError + +TurnDoneEventState = typing.Union[TurnStateCancelled, TurnStateDone, TurnStateError] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py new file mode 100644 index 000000000..9e1c299ac --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .user_message import UserMessage +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_response_event import UserToolResponseEvent + +TurnInputItem = typing.Union[UserMessage, UserToolApprovalEvent, UserToolResponseEvent] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_metrics.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_metrics.py new file mode 100644 index 000000000..85dfe8195 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_metrics.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TurnMetrics(UncheckedBaseModel): + """ + Optional billable aggregate for the whole turn. + """ + + total_cache_read_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total cache-read tokens across model calls in this turn. + """ + + total_cache_write_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total cache-write tokens across model calls in this turn. + """ + + total_cost_in_usd: typing.Optional[float] = pydantic.Field(default=None) + """ + Estimated total cost in USD for this turn. + """ + + total_input_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total input tokens across model calls in this turn. + """ + + total_output_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total output tokens across model calls in this turn. + """ + + total_reasoning_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total reasoning tokens across model calls in this turn. + """ + + total_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total tokens (input + output) across model calls in this turn. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state.py new file mode 100644 index 000000000..868df1ae3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .turn_state_cancelled import TurnStateCancelled +from .turn_state_done import TurnStateDone +from .turn_state_error import TurnStateError +from .turn_state_running import TurnStateRunning + +TurnState = typing.Union[TurnStateCancelled, TurnStateDone, TurnStateError, TurnStateRunning] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled.py new file mode 100644 index 000000000..566b56acb --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .turn_state_cancelled_metrics import TurnStateCancelledMetrics +from .turn_state_cancelled_reason import TurnStateCancelledReason + + +class TurnStateCancelled(UncheckedBaseModel): + completed_at: str = pydantic.Field() + """ + ISO 8601 time when cancellation completed. + """ + + metrics: typing.Optional[TurnStateCancelledMetrics] = pydantic.Field(default=None) + """ + Optional billable aggregate for work done before cancel. + """ + + reason: TurnStateCancelledReason + status: typing.Literal["cancelled"] = "cancelled" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled_metrics.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled_metrics.py new file mode 100644 index 000000000..5f4bdcd35 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled_metrics.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TurnStateCancelledMetrics(UncheckedBaseModel): + """ + Optional billable aggregate for work done before cancel. + """ + + total_cache_read_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total cache-read tokens across model calls in this turn. + """ + + total_cache_write_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total cache-write tokens across model calls in this turn. + """ + + total_cost_in_usd: typing.Optional[float] = pydantic.Field(default=None) + """ + Estimated total cost in USD for this turn. + """ + + total_input_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total input tokens across model calls in this turn. + """ + + total_output_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total output tokens across model calls in this turn. + """ + + total_reasoning_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total reasoning tokens across model calls in this turn. + """ + + total_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total tokens (input + output) across model calls in this turn. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled_reason.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled_reason.py new file mode 100644 index 000000000..40dfcb5b7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_cancelled_reason.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class TurnStateCancelledReason(enum.StrEnum): + """ + Reason for the cancellation. + """ + + SERVER_EXECUTION_TIMEOUT = "server-execution-timeout" + CLIENT_CANCELLED = "client-cancelled" + CANCELLED_FOR_NEXT_TURN = "cancelled-for-next-turn" + ABANDONED = "abandoned" + _UNKNOWN = "__TURNSTATECANCELLEDREASON_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "TurnStateCancelledReason": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + server_execution_timeout: typing.Callable[[], T_Result], + client_cancelled: typing.Callable[[], T_Result], + cancelled_for_next_turn: typing.Callable[[], T_Result], + abandoned: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is TurnStateCancelledReason.SERVER_EXECUTION_TIMEOUT: + return server_execution_timeout() + if self is TurnStateCancelledReason.CLIENT_CANCELLED: + return client_cancelled() + if self is TurnStateCancelledReason.CANCELLED_FOR_NEXT_TURN: + return cancelled_for_next_turn() + if self is TurnStateCancelledReason.ABANDONED: + return abandoned() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_done.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_done.py new file mode 100644 index 000000000..17ab1bf82 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_done.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .action_required_event import ActionRequiredEvent +from .model_message_event import ModelMessageEvent +from .turn_metrics import TurnMetrics + + +class TurnStateDone(UncheckedBaseModel): + completed_at: str = pydantic.Field() + """ + ISO 8601 time when the turn reached a terminal state. + """ + + metrics: typing.Optional[TurnMetrics] = None + output: typing.Optional[ModelMessageEvent] = pydantic.Field(default=None) + """ + Final `model.message` for the turn, or null when the turn ended paused without a final message. + """ + + required_actions: typing.List[ActionRequiredEvent] = pydantic.Field() + """ + Pending actions (`tool.approval_required`, `tool.response_required`, `mcp.auth_required`); empty when none. + """ + + status: typing.Literal["done"] = "done" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_error.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_error.py new file mode 100644 index 000000000..e61f1aa25 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_error.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .turn_state_error_metrics import TurnStateErrorMetrics + + +class TurnStateError(UncheckedBaseModel): + completed_at: str = pydantic.Field() + """ + ISO 8601 time when the error state was recorded. + """ + + message: str = pydantic.Field() + """ + Human-readable error message. + """ + + metrics: typing.Optional[TurnStateErrorMetrics] = pydantic.Field(default=None) + """ + Optional billable aggregate for work done before the error. + """ + + status: typing.Literal["error"] = "error" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_error_metrics.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_error_metrics.py new file mode 100644 index 000000000..161b6ceec --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_error_metrics.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TurnStateErrorMetrics(UncheckedBaseModel): + """ + Optional billable aggregate for work done before the error. + """ + + total_cache_read_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total cache-read tokens across model calls in this turn. + """ + + total_cache_write_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total cache-write tokens across model calls in this turn. + """ + + total_cost_in_usd: typing.Optional[float] = pydantic.Field(default=None) + """ + Estimated total cost in USD for this turn. + """ + + total_input_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total input tokens across model calls in this turn. + """ + + total_output_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total output tokens across model calls in this turn. + """ + + total_reasoning_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total reasoning tokens across model calls in this turn. + """ + + total_tokens: typing.Optional[int] = pydantic.Field(default=None) + """ + Total tokens (input + output) across model calls in this turn. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_running.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_running.py new file mode 100644 index 000000000..b155b427b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_state_running.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TurnStateRunning(UncheckedBaseModel): + status: typing.Literal["running"] = "running" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py new file mode 100644 index 000000000..be08f0c0a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .mcp_auth_required_event import McpAuthRequiredEvent +from .mcp_initialize_event import McpInitializeEvent +from .model_message_delta_event import ModelMessageDeltaEvent +from .model_message_event import ModelMessageEvent +from .sandbox_created_event import SandboxCreatedEvent +from .thread_created_event import ThreadCreatedEvent +from .thread_done_event import ThreadDoneEvent +from .tool_approval_required_event import ToolApprovalRequiredEvent +from .tool_response_event import ToolResponseEvent +from .tool_response_required_event import ToolResponseRequiredEvent +from .turn_created_event import TurnCreatedEvent +from .turn_done_event import TurnDoneEvent + +TurnStreamingEvent = typing.Union[ + McpAuthRequiredEvent, + McpInitializeEvent, + ModelMessageEvent, + ModelMessageDeltaEvent, + SandboxCreatedEvent, + ThreadCreatedEvent, + ThreadDoneEvent, + ToolApprovalRequiredEvent, + ToolResponseEvent, + ToolResponseRequiredEvent, + TurnCreatedEvent, + TurnDoneEvent, +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_message.py new file mode 100644 index 000000000..8f836984f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_message.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .user_message_content import UserMessageContent + + +class UserMessage(UncheckedBaseModel): + content: UserMessageContent = pydantic.Field() + """ + Plain string or structured text/file content parts. + """ + + type: typing.Literal["user.message"] = "user.message" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_message_content.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_message_content.py new file mode 100644 index 000000000..9eafe386f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_message_content.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .user_message_content_item import UserMessageContentItem + +UserMessageContent = typing.Union[str, typing.List[UserMessageContentItem]] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_message_content_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_message_content_item.py new file mode 100644 index 000000000..258bb5305 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_message_content_item.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .file_content import FileContent +from .text_content import TextContent + +UserMessageContentItem = typing.Union[FileContent, TextContent] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py new file mode 100644 index 000000000..79939dc2c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .approval_decision import ApprovalDecision + + +class UserToolApprovalEvent(UncheckedBaseModel): + approval: ApprovalDecision + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call id being approved or denied. + """ + + type: typing.Literal["user.tool_approval"] = "user.tool_approval" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py new file mode 100644 index 000000000..0fa843bf1 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UserToolResponseEvent(UncheckedBaseModel): + content: str = pydantic.Field() + """ + Client-side tool result content. + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call id receiving the client response. + """ + + type: typing.Literal["user.tool_response"] = "user.tool_response" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/zai_model_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/zai_model_provider.py new file mode 100644 index 000000000..9db33a1a2 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/zai_model_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_model import ConfiguredModel +from .model_provider_auth import ModelProviderAuth + + +class ZaiModelProvider(UncheckedBaseModel): + auth: ModelProviderAuth + base_url: typing.Optional[str] = pydantic.Field(default="https://api.z.ai/api/paas/v4") + """ + Override of the provider's default API base URL. + """ + + models: typing.List[ConfiguredModel] = pydantic.Field() + """ + Models exposed by this provider (at least one). + """ + + type: typing.Literal["zai"] = "zai" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/version.py b/python/trueforge_sdk/src/trueforge_sdk/version.py new file mode 100644 index 000000000..c28b650cf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("trueforge_sdk") diff --git a/python/trueforge_sdk/tests/conftest.py b/python/trueforge_sdk/tests/conftest.py new file mode 100644 index 000000000..25710dbe6 --- /dev/null +++ b/python/trueforge_sdk/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def _has_httpx_aiohttp() -> bool: + """Check if httpx_aiohttp is importable.""" + try: + import httpx_aiohttp # type: ignore[import-not-found] # noqa: F401 + + return True + except ImportError: + return False + + +def pytest_collection_modifyitems(config: pytest.Config, items: list) -> None: + """Auto-skip @pytest.mark.aiohttp tests when httpx_aiohttp is not installed.""" + if _has_httpx_aiohttp(): + return + skip_aiohttp = pytest.mark.skip(reason="httpx_aiohttp not installed") + for item in items: + if "aiohttp" in item.keywords: + item.add_marker(skip_aiohttp) diff --git a/python/trueforge_sdk/tests/test_aiohttp_autodetect.py b/python/trueforge_sdk/tests/test_aiohttp_autodetect.py new file mode 100644 index 000000000..263bb3c01 --- /dev/null +++ b/python/trueforge_sdk/tests/test_aiohttp_autodetect.py @@ -0,0 +1,116 @@ +import importlib +import sys +import unittest +from unittest import mock + +import httpx +import pytest + + +class TestMakeDefaultAsyncClientWithoutAiohttp(unittest.TestCase): + """Tests for _make_default_async_client when httpx_aiohttp is NOT installed.""" + + def test_returns_httpx_async_client(self) -> None: + """When httpx_aiohttp is not installed, returns plain httpx.AsyncClient.""" + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + from trueforge_sdk.base_client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=True) + self.assertIsInstance(client, httpx.AsyncClient) + self.assertEqual(client.timeout.read, 60) + self.assertTrue(client.follow_redirects) + + def test_follow_redirects_none(self) -> None: + """When follow_redirects is None, omits it from httpx.AsyncClient.""" + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + from trueforge_sdk.base_client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=None) + self.assertIsInstance(client, httpx.AsyncClient) + self.assertFalse(client.follow_redirects) + + def test_explicit_httpx_client_bypasses_autodetect(self) -> None: + """When user passes httpx_client explicitly, _make_default_async_client is not called.""" + + explicit_client = httpx.AsyncClient(timeout=120) + with mock.patch("trueforge_sdk.base_client._make_default_async_client") as mock_make: + # Replicate the generated conditional: httpx_client if httpx_client is not None else _make_default_async_client(...) + result = explicit_client if explicit_client is not None else mock_make(timeout=60, follow_redirects=True) + mock_make.assert_not_called() + self.assertIs(result, explicit_client) + + +@pytest.mark.aiohttp +class TestMakeDefaultAsyncClientWithAiohttp(unittest.TestCase): + """Tests for _make_default_async_client when httpx_aiohttp IS installed.""" + + def test_returns_aiohttp_client(self) -> None: + """When httpx_aiohttp is installed, returns HttpxAiohttpClient.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from trueforge_sdk.base_client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=True) + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertEqual(client.timeout.read, 60) + self.assertTrue(client.follow_redirects) + + def test_follow_redirects_none(self) -> None: + """When httpx_aiohttp is installed and follow_redirects is None, omits it.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from trueforge_sdk.base_client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=None) + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertFalse(client.follow_redirects) + + +class TestDefaultClientsWithoutAiohttp(unittest.TestCase): + """Tests for _default_clients.py convenience classes (no aiohttp).""" + + def test_default_async_httpx_client_defaults(self) -> None: + """DefaultAsyncHttpxClient applies SDK defaults.""" + from trueforge_sdk._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAsyncHttpxClient + + client = DefaultAsyncHttpxClient() + self.assertIsInstance(client, httpx.AsyncClient) + self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) + self.assertTrue(client.follow_redirects) + + def test_default_async_httpx_client_overrides(self) -> None: + """DefaultAsyncHttpxClient allows overriding defaults.""" + from trueforge_sdk._default_clients import DefaultAsyncHttpxClient + + client = DefaultAsyncHttpxClient(timeout=30, follow_redirects=False) + self.assertEqual(client.timeout.read, 30) + self.assertFalse(client.follow_redirects) + + def test_default_aiohttp_client_raises_without_package(self) -> None: + """DefaultAioHttpClient raises RuntimeError when httpx_aiohttp not installed.""" + import trueforge_sdk._default_clients + + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + importlib.reload(trueforge_sdk._default_clients) + + with self.assertRaises(RuntimeError) as ctx: + trueforge_sdk._default_clients.DefaultAioHttpClient() + self.assertIn("pip install trueforge_sdk[aiohttp]", str(ctx.exception)) + + importlib.reload(trueforge_sdk._default_clients) + + +@pytest.mark.aiohttp +class TestDefaultClientsWithAiohttp(unittest.TestCase): + """Tests for _default_clients.py when httpx_aiohttp IS installed.""" + + def test_default_aiohttp_client_defaults(self) -> None: + """DefaultAioHttpClient works when httpx_aiohttp is installed.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from trueforge_sdk._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAioHttpClient + + client = DefaultAioHttpClient() + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) + self.assertTrue(client.follow_redirects) diff --git a/scripts/generate-sdk.sh b/scripts/generate-sdk.sh index 39dcc0453..b2fb8cdbb 100755 --- a/scripts/generate-sdk.sh +++ b/scripts/generate-sdk.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Regenerates OpenAPI (.github/fern + docs/) and packages/trueforge-sdk (same steps as CI). +# Regenerates OpenAPI (.github/fern + docs/), packages/trueforge-sdk, and +# python/trueforge_sdk (same steps as CI). # Requires Docker (Fern --local) and network access for the Fern CLI/image. set -euo pipefail @@ -12,16 +13,34 @@ fern() { (cd .github && pnpm dlx "fern-api@${fern_version}" "$@") } -# Fern --version stamps package.json and baked-in TS literals (SDK_VERSION, -# User-Agent). Pass the version already in package.json so a regen never stomps -# a version changesets (or a human) already set. -current_version="$(node -p "require('./packages/trueforge-sdk/package.json').version")" +# Fern --version stamps package metadata and baked-in SDK version / User-Agent. +# Pass the version already in each package's metadata so a regen never stomps a +# version a human (or Changesets, for TS) already set. +ts_version="$(node -p "require('./packages/trueforge-sdk/package.json').version")" +# Python has its own line (not Changesets/npm). Prefer pyproject.toml when present. +py_version="0.1.0-rc.1" +if [[ -f python/trueforge_sdk/pyproject.toml ]]; then + py_version="$(node -p 'require("fs").readFileSync("python/trueforge_sdk/pyproject.toml","utf8").match(/^version\s*=\s*"([^"]+)"/m)[1]')" +fi pnpm --filter @truefoundry/trueforge-core build pnpm openapi:write fern check -# --force skips the overwrite prompt when packages/trueforge-sdk already exists (needed non-interactively / in CI). -fern generate --group ts-sdk --version "$current_version" --local --generate-tests --force --log-level debug +# --force skips the overwrite prompt when the SDK dirs already exist (needed non-interactively / in CI). +fern generate --group ts-sdk --version "$ts_version" --local --generate-tests --force --log-level debug +fern generate --group python-sdk --version "$py_version" --local --generate-tests --force --log-level debug +test -f python/trueforge_sdk/src/trueforge_sdk/client.py +# local-file-system has no output.package-name; Fern stamps import name into Poetry too. +# Rewrite dist name to trueforge-sdk (import path stays trueforge_sdk). +python3 -c ' +from pathlib import Path +p = Path("python/trueforge_sdk/pyproject.toml") +t = p.read_text() +n = t.replace("name = \"trueforge_sdk\"", "name = \"trueforge-sdk\"") +if n == t: + raise SystemExit("expected Fern pyproject name = \"trueforge_sdk\"") +p.write_text(n) +' # Fern's generated verify.sh runs `pnpm install` from packages/trueforge-sdk, which now # resolves to this workspace. CI sets frozen-lockfile, so refresh the root # lockfile first or that install fails when the generator added/removed deps.