diff --git a/.github/workflows/oasdiff-comment.yml b/.github/workflows/oasdiff-comment.yml new file mode 100644 index 00000000..55fd96a3 --- /dev/null +++ b/.github/workflows/oasdiff-comment.yml @@ -0,0 +1,63 @@ +name: OpenAPI Diff Comment + +# Split from oasdiff.yml specifically so this job — the only one that needs a write-capable +# GITHUB_TOKEN — never checks out, fetches, or executes anything from the PR/fork. It only +# downloads the plain-text report artifact that oasdiff.yml (running with a read-only token, +# on `pull_request`) already produced, and posts/updates it as a PR comment. This is GitHub's +# own documented pattern for safely commenting on fork PRs — see: +# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ +on: + workflow_run: + workflows: ["OpenAPI Diff (oasdiff)"] + types: + - completed + +permissions: + actions: read + pull-requests: write + +jobs: + comment: + name: Post oasdiff report comment + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + steps: + - name: Download report artifact + uses: actions/download-artifact@v4 + with: + name: oasdiff-report + path: /tmp/oasdiff + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const prNumber = parseInt(fs.readFileSync('/tmp/oasdiff/pr_number.txt', 'utf8').trim(), 10); + const body = marker + '\n' + fs.readFileSync('/tmp/oasdiff/report.md', 'utf8'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + const existing = comments.find(c => c.body && c.body.startsWith(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } diff --git a/.github/workflows/oasdiff.yml b/.github/workflows/oasdiff.yml new file mode 100644 index 00000000..9fa32afb --- /dev/null +++ b/.github/workflows/oasdiff.yml @@ -0,0 +1,115 @@ +name: OpenAPI Diff (oasdiff) + +# Deliberately plain `pull_request` (not `_target`), so this job never gets a write-capable +# GITHUB_TOKEN and never needs one — it only reads spec JSON and writes a report. GitHub always +# gives `pull_request` a read-only token for fork PRs regardless of `permissions:` below, which +# is exactly what we want here: this job has nothing to write. Posting the sticky PR comment is +# the separate, privileged job in .github/workflows/oasdiff-comment.yml (triggered by +# `workflow_run` once this one finishes), which never checks out or executes anything from the +# PR — see that file for why that split exists. +on: + pull_request: + paths: + - 'api/**' + - '.github/workflows/oasdiff.yml' + +permissions: + contents: read + +jobs: + oasdiff: + name: oasdiff breaking-change report + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve base/head OpenAPI spec files + id: specs + run: | + set -euo pipefail + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + mkdir -p /tmp/oasdiff + + base_spec=$(git ls-tree -r --name-only "$BASE_SHA" -- api | grep -E '^api/graphiant_api_docs_v[0-9.]+\.json$' | sort -V | tail -1 || true) + head_spec=$(git ls-tree -r --name-only "$HEAD_SHA" -- api | grep -E '^api/graphiant_api_docs_v[0-9.]+\.json$' | sort -V | tail -1 || true) + + if [ -z "$base_spec" ] || [ -z "$head_spec" ]; then + echo "No versioned OpenAPI spec found on one side of the diff — skipping oasdiff." | tee /tmp/oasdiff/report.md >> "$GITHUB_STEP_SUMMARY" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git show "$BASE_SHA:$base_spec" > /tmp/oasdiff/base.json + git show "$HEAD_SHA:$head_spec" > /tmp/oasdiff/head.json + + if cmp -s /tmp/oasdiff/base.json /tmp/oasdiff/head.json; then + echo "OpenAPI spec content unchanged — skipping oasdiff." | tee /tmp/oasdiff/report.md >> "$GITHUB_STEP_SUMMARY" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "base_spec=$base_spec" >> "$GITHUB_OUTPUT" + echo "head_spec=$head_spec" >> "$GITHUB_OUTPUT" + + # Emits ::error:: workflow commands, which GitHub renders as inline + # annotations on the "Files changed" tab and in the job's Checks output. + - name: Annotate breaking changes + if: steps.specs.outputs.skip != 'true' + run: | + docker run --rm --mount type=bind,src=/tmp/oasdiff,dst=/data tufin/oasdiff breaking \ + --format githubactions /data/base.json /data/head.json \ + | sed 's#/data/head\.json#${{ steps.specs.outputs.head_spec }}#g' + + - name: Build oasdiff report + if: steps.specs.outputs.skip != 'true' + run: | + { + echo "## oasdiff: \`${{ steps.specs.outputs.base_spec }}\` → \`${{ steps.specs.outputs.head_spec }}\`" + echo + docker run --rm --mount type=bind,src=/tmp/oasdiff,dst=/data tufin/oasdiff breaking \ + --format text /data/base.json /data/head.json > /tmp/oasdiff/breaking.txt || true + + # Each breaking-change finding is a line starting with "error" (tab-separated); the + # summary/no-change lines never start a line that way, so this is more robust than + # matching oasdiff's exact wording, which differs between "no changes at all" and + # "changes, but none breaking". + if ! grep -qE '^error\b' /tmp/oasdiff/breaking.txt; then + echo "✅ **No breaking changes** detected by [oasdiff](https://github.com/oasdiff/oasdiff)." + else + echo "⚠️ **oasdiff reports non-backward-compatible changes** in this spec update — review before releasing:" + echo + echo '```' + cat /tmp/oasdiff/breaking.txt + echo '```' + fi + + echo + echo "
Full spec changelog (oasdiff)" + echo + docker run --rm --mount type=bind,src=/tmp/oasdiff,dst=/data tufin/oasdiff changelog \ + --format markdown /data/base.json /data/head.json || true + echo + echo "
" + echo + echo "_Run \`bash scripts/oasdiff.sh\` locally to reproduce, or \`make oasdiff-changelog\` for the full changelog (requires Docker)._" + } | tee /tmp/oasdiff/report.md >> "$GITHUB_STEP_SUMMARY" + + # Always runs (report.md exists whether or not oasdiff itself ran — the resolve step above + # writes a short placeholder report in the skip case) so oasdiff-comment.yml can always + # find an artifact to download, with no special-casing on its end. + - name: Save PR number + run: echo "${{ github.event.pull_request.number }}" > /tmp/oasdiff/pr_number.txt + + - name: Upload report artifact + uses: actions/upload-artifact@v4 + with: + name: oasdiff-report + path: | + /tmp/oasdiff/report.md + /tmp/oasdiff/pr_number.txt + retention-days: 1 diff --git a/.gitignore b/.gitignore index 5ec426f7..c27b10bf 100644 --- a/.gitignore +++ b/.gitignore @@ -175,4 +175,6 @@ cython_debug/ .DS_Store .openapi-generator -git_push.sh \ No newline at end of file +git_push.sh +# oasdiff local scratch file +api/.oasdiff-base.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3303bc5d..954b969a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +## [26.8.0] - 2026-08-26 + +> **⚠️ Breaking change in the underlying API** (flagged by [oasdiff](https://github.com/oasdiff/oasdiff); not a SemVer-major bump since this API doesn't follow strict SemVer): the Zendesk integration endpoints (`POST /v2/integration/`, `PUT /v2/integration/{integrationId}`, `GET /v2/integration/getall/{enterpriseId}`) now **require** `zendeskClientId`/`zendeskClientSecret` in the request body in place of `zendeskApiToken`/`zendeskEmail`. See `AlertserviceZendeskDetails` below. + +### Added +- **API spec:** `graphiant_api_docs_v26.8.0.json` +- **Adaptive FEC:** **`PUT /v1/adaptive-fec`** — update adaptive FEC settings for an enterprise; models **`V1AdaptiveFecPutRequest`**, **`ManaV2AdaptiveFecConfiguration`** (`adaptive_fec_enabled`, `sla_classes`) +- **`V1EnterpriseConfigurationGetResponse`**: added `adaptive_fec_config` field (**`ManaV2AdaptiveFecConfiguration`**) +- **`AlertserviceZendeskDetails`**: added `zendesk_client_id` and `zendesk_client_secret` fields (Zendesk OAuth credentials) — now **required** in place of the deprecated `zendesk_api_token`/`zendesk_email` +- **`GET /v1/extranet/b2b/services/summary`**: added optional `serviceType` query parameter +- **CI/tooling:** `OpenAPI Diff (oasdiff)` workflow — [oasdiff](https://github.com/oasdiff/oasdiff) breaking-change report on any PR touching `api/graphiant_api_docs_*.json`; `make oasdiff` / `make oasdiff-changelog` reproduce it locally + +### Changed +- **Version:** Package **26.8.0**; OpenAPI bundle **`graphiant_api_docs_v26.8.0.json`** (replaces **26.7.0** bundle in-repo). +- **`AlertserviceZendeskDetails`**: `zendesk_api_token` and `zendesk_email` are now optional in the schema (deprecated in favor of `zendesk_client_secret`/`zendesk_client_id`), but the underlying `/v2/integration/*` endpoints now require the new OAuth fields — see breaking-change note above. +- **SDK (generated):** Refreshed **`default_api`**, model exports, and **`docs/`** to match the **26.8.0** spec. + +### Removed +- **API spec:** removed **`graphiant_api_docs_v26.7.0.json`** (superseded by **26.8.0** bundle). + ## [26.7.0] - 2026-07-27 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3d4144a..68d7f8bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,6 +129,19 @@ make generate Review `git diff` carefully after generation — pay particular attention to files in `.openapi-generator-ignore` to confirm they were not overwritten. +### Checking for breaking API changes (oasdiff) + +Any PR that touches `api/graphiant_api_docs_*.json` automatically gets an [oasdiff](https://github.com/oasdiff/oasdiff) breaking-change report — a job summary and inline annotations on the spec file from `OpenAPI Diff (oasdiff)` (`.github/workflows/oasdiff.yml`), plus a sticky PR comment (updated in place on each push, not duplicated) from a follow-up job, `OpenAPI Diff Comment` (`.github/workflows/oasdiff-comment.yml`). That job is split out and triggered via `workflow_run` specifically so it can safely get a write-capable token for PRs from forks, without ever needing to check out or execute anything from the PR itself — see the comments in that file for the reasoning. No need to run any of this manually before opening a PR, but you can reproduce it locally while drafting the `CHANGELOG.md` entry: + +```bash +make oasdiff # breaking-change report +make oasdiff-changelog # full markdown changelog of every spec change +``` + +Requires Docker (pulls `tufin/oasdiff`). By default it diffs the two highest-versioned specs found in `api/` if you've dropped a new one in alongside the old one, otherwise the latest local spec against the version committed on `origin/main`. + +The CI check is informational, not a merge gate — the Graphiant API doesn't follow strict semver, so a spec bump can legitimately include breaking changes. If oasdiff flags something, call it out explicitly in the PR description and `CHANGELOG.md` entry rather than silently letting the generated SDK diff speak for itself. + ## Code Standards ### Python Code @@ -300,6 +313,13 @@ If tests fail: 2. Run specific test: `pytest tests/test_file.py::test_function -v` 3. Check test coverage: `pytest --cov=graphiant_sdk --cov-report=term tests/` +### Unexpected model changes after `make generate` + +`openapi-generator` deduplicates inline anonymous response schemas (e.g. simple `{"error": "string"}` error bodies reused across several endpoints) by content hash, and which model name "wins" the dedup is not stable across runs — even with an unchanged spec and the same generator version. If `git diff` after regenerating shows response model classes disappearing from `graphiant_sdk/__init__.py` / `graphiant_sdk/models/__init__.py` or being renamed in `default_api.py`'s `_response_types_map`, for endpoints your spec change didn't touch: + +1. Confirm it's not a real spec change: diff the specific path's `responses` block between old and new spec JSON directly. +2. If unchanged, it's generator nondeterminism — restore the class names/exports that are on `main` today rather than let them silently drop from the SDK's public API as a side effect of an unrelated spec update. + ### Merge Conflicts If you have merge conflicts: diff --git a/Makefile b/Makefile index 92165597..96d9842d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install test lint type-check build generate clean help +.PHONY: install test lint type-check build generate oasdiff oasdiff-changelog clean help ## install: Install package in editable mode with dev dependencies install: @@ -30,6 +30,14 @@ build: generate: @bash scripts/generate.sh +## oasdiff: Check the OpenAPI spec diff for breaking changes (requires Docker) +oasdiff: + @bash scripts/oasdiff.sh + +## oasdiff-changelog: Full markdown changelog of OpenAPI spec changes (requires Docker) +oasdiff-changelog: + @MODE=changelog bash scripts/oasdiff.sh + ## clean: Remove build artifacts clean: rm -rf dist/ build/ *.egg-info/ diff --git a/README.md b/README.md index 1cde94d6..c644079f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ More product and platform context: [Graphiant Docs](https://docs.graphiant.com). | **Automation** | [Graphiant Automation](https://docs.graphiant.com/docs/automation) | | **REST API** | [Graphiant Portal REST API](https://docs.graphiant.com/docs/graphiant-portal-rest-api) | | **Method index (repo)** | [DefaultApi.md](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/docs/DefaultApi.md) | -| **OpenAPI bundle (this build)** | [`api/graphiant_api_docs_v26.7.0.json`](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/api/graphiant_api_docs_v26.7.0.json) — source for generated paths and models | +| **OpenAPI bundle (this build)** | [`api/graphiant_api_docs_v26.8.0.json`](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/api/graphiant_api_docs_v26.8.0.json) — source for generated paths and models | | **Model docs (`*.md`)** | [`docs/`](https://github.com/Graphiant-Inc/graphiant-sdk-python/tree/main/docs) (same names as Python classes, e.g. `V1EdgesSummaryGetResponse.md`) | | **PyPI** | [graphiant-sdk](https://pypi.org/project/graphiant-sdk) | | **Changelog** | [CHANGELOG.md](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/CHANGELOG.md) | @@ -462,6 +462,12 @@ make type-check make build # python -m build ``` +Build artifacts land in `dist/` (`graphiant_sdk-.tar.gz` sdist, `graphiant_sdk--py3-none-any.whl` wheel). To try a build before it's published to PyPI — e.g. in another project or a test-infrastructure repo — install directly from the file: + +```bash +pip install /path/to/graphiant-sdk-python/dist/graphiant_sdk-26.8.0.tar.gz +``` + Or without `make`: ```bash @@ -477,12 +483,26 @@ To regenerate the SDK from the latest API specification: make generate # Or run the script directly (supports OPENAPI_SPEC override) -OPENAPI_SPEC=api/graphiant_api_docs_v26.7.0.json bash scripts/generate.sh +OPENAPI_SPEC=api/graphiant_api_docs_v26.8.0.json bash scripts/generate.sh ``` `scripts/generate.sh` wraps the full `openapi-generator-cli generate` invocation (requires OpenAPI Generator **>= 7.23.0**). It auto-detects `openapi-generator` (Homebrew) or `openapi-generator-cli` (npm), reads the current version from `pyproject.toml`, and passes `--git-user-id`/`--git-repo-id` so generated docs never contain `GIT_USER_ID` placeholders. -> **Note:** Download the latest API bundle from the Graphiant portal under **Support Hub** → **Developer Tools** and place it in `api/`. The versioned JSON bundle (`api/graphiant_api_docs_v26.7.0.json`) is the snapshot used for this release; `api/openapi.yaml` is the primary YAML spec. Hand-written files listed in `.openapi-generator-ignore` (`graphiant_cli/`, `graphiant_sdk/api_client.py`, `configuration.py`, etc.) are never overwritten by the generator. +> **Note:** Download the latest API bundle from the Graphiant portal under **Support Hub** → **Developer Tools** and place it in `api/`. The versioned JSON bundle (`api/graphiant_api_docs_v26.8.0.json`) is the snapshot used for this release; `api/openapi.yaml` is the primary YAML spec. Hand-written files listed in `.openapi-generator-ignore` (`graphiant_cli/`, `graphiant_sdk/api_client.py`, `configuration.py`, etc.) are never overwritten by the generator. + +#### Checking for breaking API changes (oasdiff) + +Any PR touching `api/graphiant_api_docs_*.json` gets an automated [oasdiff](https://github.com/oasdiff/oasdiff) breaking-change report from the `OpenAPI Diff (oasdiff)` GitHub Actions workflow — a job summary, inline annotations, and a sticky PR comment (updated in place on each push). To reproduce it locally (requires Docker) — handy for figuring out what to write in the `CHANGELOG.md` entry for a spec update: + +```bash +make oasdiff # breaking-change report +make oasdiff-changelog # full markdown changelog of every spec change + +# Or against explicit files: +bash scripts/oasdiff.sh api/graphiant_api_docs_v26.7.0.json api/graphiant_api_docs_v26.8.0.json +``` + +The check is informational, not a merge gate — this API doesn't follow strict SemVer, so a spec bump can legitimately include breaking changes. See `CONTRIBUTING.md` for details. ### Testing @@ -498,7 +518,7 @@ pytest tests/ --cov=graphiant_sdk --cov-report=html ### Source of truth (this release) -Operations and schemas are generated from **`api/graphiant_api_docs_v26.7.0.json`** (in `api/` and bundled in the PyPI wheel). For a newer portal/API, download the current bundle (Support Hub → Developer Tools) and diff paths before relying on URLs here. +Operations and schemas are generated from **`api/graphiant_api_docs_v26.8.0.json`** (in `api/` and bundled in the PyPI wheel). For a newer portal/API, download the current bundle (Support Hub → Developer Tools) and diff paths before relying on URLs here. | How to explore | Where | |----------------|-------| @@ -529,7 +549,7 @@ REST query parameters use **camelCase** in URLs (`enterpriseId`). Generated Pyth | **`V1GlobalSummaryPostRequest`**, **`V1GlobalSummaryPostResponse`** | **`POST /v1/global/summary`** | | **`V2ParentalertlistPostRequest`**, **`V2ParentalertlistPostResponse`** | **`POST /v2/parentalertlist`** | -### Sample HTTP endpoints (from `graphiant_api_docs_v26.7.0.json`) +### Sample HTTP endpoints (from `graphiant_api_docs_v26.8.0.json`) The API surface is large; this table lists **real** paths from the bundled spec. For the full set, use **`graphiant api list`** or **`DefaultApi.md`**. diff --git a/SECURITY.md b/SECURITY.md index 844ecb20..7c23f383 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,9 @@ We actively support the following versions with security updates: | Version | Supported | Notes | |---------| ------------------ |----------------------------------------------| -| 26.7.x | :white_check_mark: | Current stable release (latest: **26.7.0**) | -| 26.6.x | :white_check_mark: | Previous stable release (latest: **26.6.0**) | +| 26.8.x | :white_check_mark: | Current stable release (latest: **26.8.0**) | +| 26.7.x | :white_check_mark: | Previous stable release (latest: **26.7.0**) | +| 26.6.x | :white_check_mark: | Older release line (latest: **26.6.0**) | | 26.5.x | :white_check_mark: | Older release line (latest: **26.5.0**) | | 26.4.x | :white_check_mark: | Older supported release (latest: **26.4.0**) | | 26.3.x | :white_check_mark: | Older supported release (latest: **26.3.3**) | @@ -278,4 +279,4 @@ For security concerns, please contact: **security@graphiant.com** --- -**Last Updated**: 2026-07-27 +**Last Updated**: 2026-08-26 diff --git a/api/graphiant_api_docs_v26.7.0.json b/api/graphiant_api_docs_v26.8.0.json similarity index 99% rename from api/graphiant_api_docs_v26.7.0.json rename to api/graphiant_api_docs_v26.8.0.json index 0ec3bb1f..b33645fc 100644 --- a/api/graphiant_api_docs_v26.7.0.json +++ b/api/graphiant_api_docs_v26.8.0.json @@ -682,7 +682,7 @@ "zendeskApiToken": { "type": "string", "example": "example string", - "description": "zendesk api token (required)" + "description": "zendesk api token (deprecated, use zendesk_client_secret)" }, "zendeskAssigneeId": { "type": "string", @@ -694,18 +694,28 @@ "example": "example string", "description": "zendesk base url (required)" }, + "zendeskClientId": { + "type": "string", + "example": "example string", + "description": "zendesk oauth client id (required)" + }, + "zendeskClientSecret": { + "type": "string", + "example": "example string", + "description": "zendesk oauth client secret (required)" + }, "zendeskEmail": { "type": "string", "example": "example string", - "description": "zendesk email (required)" + "description": "zendesk email (deprecated, use zendesk_client_id)" } }, "additionalProperties": false, "required": [ - "zendeskApiToken", "zendeskAssigneeId", "zendeskBaseUrl", - "zendeskEmail" + "zendeskClientId", + "zendeskClientSecret" ] }, "v2IntegrationPostResponse": { @@ -26744,12 +26754,32 @@ "v1EnterpriseConfigurationGetResponse": { "type": "object", "properties": { + "adaptiveFecConfig": { + "$ref": "#/components/schemas/manaV2AdaptiveFecConfiguration" + }, "configuration": { "$ref": "#/components/schemas/manaV2EnterpriseConfiguration" } }, "additionalProperties": false }, + "manaV2AdaptiveFecConfiguration": { + "type": "object", + "properties": { + "adaptiveFecEnabled": { + "type": "boolean", + "example": true + }, + "slaClasses": { + "type": "array", + "items": { + "type": "string", + "example": "ENUM_VALUE" + } + } + }, + "additionalProperties": false + }, "v1EnterpriseContractPutRequest": { "type": "object", "properties": { @@ -28049,6 +28079,20 @@ "properties": {}, "additionalProperties": false }, + "v1AdaptiveFecPutRequest": { + "type": "object", + "properties": { + "configuration": { + "$ref": "#/components/schemas/manaV2AdaptiveFecConfiguration" + } + }, + "additionalProperties": false + }, + "v1AdaptiveFecPutResponse": { + "type": "object", + "properties": {}, + "additionalProperties": false + }, "v1DevicesOauthPostRequest": { "type": "object", "properties": { @@ -49272,6 +49316,16 @@ "type": "string" }, "description": "Bearer token. Format: Bearer " + }, + { + "name": "serviceType", + "in": "query", + "description": "Optional filter by branded service type", + "required": false, + "schema": { + "type": "string", + "example": "ENUM_VALUE" + } } ], "responses": { @@ -52117,6 +52171,49 @@ "description": "Tries synching device with the global objects" } }, + "/v1/adaptive-fec": { + "put": { + "security": [ + { + "jwtAuth": [] + } + ], + "parameters": [ + { + "in": "header", + "name": "Authorization", + "required": true, + "schema": { + "type": "string" + }, + "description": "Bearer token. Format: Bearer " + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v1AdaptiveFecPutRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v1AdaptiveFecPutResponse" + } + } + } + } + }, + "description": "Update adaptive FEC settings for an enterprise" + } + }, "/v1/extranets-b2b-peering/consumer/{id}/prefixes": { "put": { "security": [ diff --git a/docs/AlertserviceZendeskDetails.md b/docs/AlertserviceZendeskDetails.md index 7b2c1977..7181c14e 100644 --- a/docs/AlertserviceZendeskDetails.md +++ b/docs/AlertserviceZendeskDetails.md @@ -5,10 +5,12 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**zendesk_api_token** | **str** | zendesk api token (required) | +**zendesk_api_token** | **str** | zendesk api token (deprecated, use zendesk_client_secret) | [optional] **zendesk_assignee_id** | **str** | zendesk assignee id (required) | **zendesk_base_url** | **str** | zendesk base url (required) | -**zendesk_email** | **str** | zendesk email (required) | +**zendesk_client_id** | **str** | zendesk oauth client id (required) | +**zendesk_client_secret** | **str** | zendesk oauth client secret (required) | +**zendesk_email** | **str** | zendesk email (deprecated, use zendesk_client_id) | [optional] ## Example diff --git a/docs/DefaultApi.md b/docs/DefaultApi.md index 1bf65dae..bb33978c 100644 --- a/docs/DefaultApi.md +++ b/docs/DefaultApi.md @@ -12,6 +12,7 @@ Method | HTTP request | Description [**v1_account_mfa_put**](DefaultApi.md#v1_account_mfa_put) | **PUT** /v1/account/mfa | [**v1_account_password_patch**](DefaultApi.md#v1_account_password_patch) | **PATCH** /v1/account/password | [**v1_activity_logs_post**](DefaultApi.md#v1_activity_logs_post) | **POST** /v1/activity/logs | +[**v1_adaptive_fec_put**](DefaultApi.md#v1_adaptive_fec_put) | **PUT** /v1/adaptive-fec | [**v1_apps_app_summary_post**](DefaultApi.md#v1_apps_app_summary_post) | **POST** /v1/apps/app-summary | [**v1_apps_bandwidth_post**](DefaultApi.md#v1_apps_bandwidth_post) | **POST** /v1/apps/bandwidth | [**v1_apps_device_device_id_top_post**](DefaultApi.md#v1_apps_device_device_id_top_post) | **POST** /v1/apps/device/{deviceId}/top | @@ -1143,6 +1144,84 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **v1_adaptive_fec_put** +> object v1_adaptive_fec_put(authorization, v1_adaptive_fec_put_request) + +Update adaptive FEC settings for an enterprise + +### Example + +* Api Key Authentication (jwtAuth): + +```python +import graphiant_sdk +from graphiant_sdk.models.v1_adaptive_fec_put_request import V1AdaptiveFecPutRequest +from graphiant_sdk.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://api.graphiant.com +# See configuration.py for a list of all supported configuration parameters. +configuration = graphiant_sdk.Configuration( + host = "https://api.graphiant.com" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +# Configure API key authorization: jwtAuth +configuration.api_key['jwtAuth'] = os.environ["API_KEY"] + +# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed +# configuration.api_key_prefix['jwtAuth'] = 'Bearer' + +# Enter a context with an instance of the API client +with graphiant_sdk.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = graphiant_sdk.DefaultApi(api_client) + authorization = 'authorization_example' # str | Bearer token. Format: Bearer + v1_adaptive_fec_put_request = graphiant_sdk.V1AdaptiveFecPutRequest() # V1AdaptiveFecPutRequest | + + try: + api_response = api_instance.v1_adaptive_fec_put(authorization, v1_adaptive_fec_put_request) + print("The response of DefaultApi->v1_adaptive_fec_put:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling DefaultApi->v1_adaptive_fec_put: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **authorization** | **str**| Bearer token. Format: Bearer <your_token_here> | + **v1_adaptive_fec_put_request** | [**V1AdaptiveFecPutRequest**](V1AdaptiveFecPutRequest.md)| | + +### Return type + +**object** + +### Authorization + +[jwtAuth](../README.md#jwtAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | OK | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **v1_apps_app_summary_post** > V1AppsAppSummaryPostResponse v1_apps_app_summary_post(authorization, v1_apps_app_summary_post_request) @@ -17401,7 +17480,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **v1_extranet_b2b_services_summary_get** -> V1ExtranetB2bServicesSummaryGetResponse v1_extranet_b2b_services_summary_get(authorization) +> V1ExtranetB2bServicesSummaryGetResponse v1_extranet_b2b_services_summary_get(authorization, service_type=service_type) List inter-enterprise extranet services @@ -17437,9 +17516,10 @@ with graphiant_sdk.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = graphiant_sdk.DefaultApi(api_client) authorization = 'authorization_example' # str | Bearer token. Format: Bearer + service_type = 'ENUM_VALUE' # str | Optional filter by branded service type (optional) try: - api_response = api_instance.v1_extranet_b2b_services_summary_get(authorization) + api_response = api_instance.v1_extranet_b2b_services_summary_get(authorization, service_type=service_type) print("The response of DefaultApi->v1_extranet_b2b_services_summary_get:\n") pprint(api_response) except Exception as e: @@ -17454,6 +17534,7 @@ with graphiant_sdk.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **authorization** | **str**| Bearer token. Format: Bearer <your_token_here> | + **service_type** | **str**| Optional filter by branded service type | [optional] ### Return type diff --git a/docs/ManaV2AdaptiveFecConfiguration.md b/docs/ManaV2AdaptiveFecConfiguration.md new file mode 100644 index 00000000..1fde29c9 --- /dev/null +++ b/docs/ManaV2AdaptiveFecConfiguration.md @@ -0,0 +1,30 @@ +# ManaV2AdaptiveFecConfiguration + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**adaptive_fec_enabled** | **bool** | | [optional] +**sla_classes** | **List[str]** | | [optional] + +## Example + +```python +from graphiant_sdk.models.mana_v2_adaptive_fec_configuration import ManaV2AdaptiveFecConfiguration + +# TODO update the JSON string below +json = "{}" +# create an instance of ManaV2AdaptiveFecConfiguration from a JSON string +mana_v2_adaptive_fec_configuration_instance = ManaV2AdaptiveFecConfiguration.from_json(json) +# print the JSON string representation of the object +print(ManaV2AdaptiveFecConfiguration.to_json()) + +# convert the object into a dict +mana_v2_adaptive_fec_configuration_dict = mana_v2_adaptive_fec_configuration_instance.to_dict() +# create an instance of ManaV2AdaptiveFecConfiguration from a dict +mana_v2_adaptive_fec_configuration_from_dict = ManaV2AdaptiveFecConfiguration.from_dict(mana_v2_adaptive_fec_configuration_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/V1AdaptiveFecPutRequest.md b/docs/V1AdaptiveFecPutRequest.md new file mode 100644 index 00000000..a3c0a780 --- /dev/null +++ b/docs/V1AdaptiveFecPutRequest.md @@ -0,0 +1,29 @@ +# V1AdaptiveFecPutRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**configuration** | [**ManaV2AdaptiveFecConfiguration**](ManaV2AdaptiveFecConfiguration.md) | | [optional] + +## Example + +```python +from graphiant_sdk.models.v1_adaptive_fec_put_request import V1AdaptiveFecPutRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of V1AdaptiveFecPutRequest from a JSON string +v1_adaptive_fec_put_request_instance = V1AdaptiveFecPutRequest.from_json(json) +# print the JSON string representation of the object +print(V1AdaptiveFecPutRequest.to_json()) + +# convert the object into a dict +v1_adaptive_fec_put_request_dict = v1_adaptive_fec_put_request_instance.to_dict() +# create an instance of V1AdaptiveFecPutRequest from a dict +v1_adaptive_fec_put_request_from_dict = V1AdaptiveFecPutRequest.from_dict(v1_adaptive_fec_put_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/V1EnterpriseConfigurationGetResponse.md b/docs/V1EnterpriseConfigurationGetResponse.md index b0ae5aed..7f2a462e 100644 --- a/docs/V1EnterpriseConfigurationGetResponse.md +++ b/docs/V1EnterpriseConfigurationGetResponse.md @@ -5,6 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**adaptive_fec_config** | [**ManaV2AdaptiveFecConfiguration**](ManaV2AdaptiveFecConfiguration.md) | | [optional] **configuration** | [**ManaV2EnterpriseConfiguration**](ManaV2EnterpriseConfiguration.md) | | [optional] ## Example diff --git a/graphiant_sdk/__init__.py b/graphiant_sdk/__init__.py index 398f27b5..e14638ca 100644 --- a/graphiant_sdk/__init__.py +++ b/graphiant_sdk/__init__.py @@ -14,7 +14,7 @@ """ # noqa: E501 -__version__ = "26.7.0" +__version__ = "26.8.0" # Define package exports __all__ = [ @@ -158,6 +158,7 @@ "IpfixTwampMetrics", "ManaV2AWSGatewayDetailsTransitConnection", "ManaV2AWSTransitGatewayVpc", + "ManaV2AdaptiveFecConfiguration", "ManaV2AllocationConversionHolder", "ManaV2AllocationConversionHolderBin", "ManaV2App", @@ -787,6 +788,7 @@ "V1ActivityLogsPostRequest", "V1ActivityLogsPostResponse", "V1ActivityLogsPostResponseActivityItems", + "V1AdaptiveFecPutRequest", "V1AppsAppSummaryPostRequest", "V1AppsAppSummaryPostResponse", "V1AppsBandwidthPostRequest", @@ -1674,6 +1676,7 @@ from graphiant_sdk.models.ipfix_twamp_metrics import IpfixTwampMetrics as IpfixTwampMetrics from graphiant_sdk.models.mana_v2_aws_gateway_details_transit_connection import ManaV2AWSGatewayDetailsTransitConnection as ManaV2AWSGatewayDetailsTransitConnection from graphiant_sdk.models.mana_v2_aws_transit_gateway_vpc import ManaV2AWSTransitGatewayVpc as ManaV2AWSTransitGatewayVpc +from graphiant_sdk.models.mana_v2_adaptive_fec_configuration import ManaV2AdaptiveFecConfiguration as ManaV2AdaptiveFecConfiguration from graphiant_sdk.models.mana_v2_allocation_conversion_holder import ManaV2AllocationConversionHolder as ManaV2AllocationConversionHolder from graphiant_sdk.models.mana_v2_allocation_conversion_holder_bin import ManaV2AllocationConversionHolderBin as ManaV2AllocationConversionHolderBin from graphiant_sdk.models.mana_v2_app import ManaV2App as ManaV2App @@ -2303,6 +2306,7 @@ from graphiant_sdk.models.v1_activity_logs_post_request import V1ActivityLogsPostRequest as V1ActivityLogsPostRequest from graphiant_sdk.models.v1_activity_logs_post_response import V1ActivityLogsPostResponse as V1ActivityLogsPostResponse from graphiant_sdk.models.v1_activity_logs_post_response_activity_items import V1ActivityLogsPostResponseActivityItems as V1ActivityLogsPostResponseActivityItems +from graphiant_sdk.models.v1_adaptive_fec_put_request import V1AdaptiveFecPutRequest as V1AdaptiveFecPutRequest from graphiant_sdk.models.v1_apps_app_summary_post_request import V1AppsAppSummaryPostRequest as V1AppsAppSummaryPostRequest from graphiant_sdk.models.v1_apps_app_summary_post_response import V1AppsAppSummaryPostResponse as V1AppsAppSummaryPostResponse from graphiant_sdk.models.v1_apps_bandwidth_post_request import V1AppsBandwidthPostRequest as V1AppsBandwidthPostRequest diff --git a/graphiant_sdk/api/default_api.py b/graphiant_sdk/api/default_api.py index 91b55a5f..7f701b76 100644 --- a/graphiant_sdk/api/default_api.py +++ b/graphiant_sdk/api/default_api.py @@ -28,6 +28,7 @@ from graphiant_sdk.models.v1_account_password_patch_request import V1AccountPasswordPatchRequest from graphiant_sdk.models.v1_activity_logs_post_request import V1ActivityLogsPostRequest from graphiant_sdk.models.v1_activity_logs_post_response import V1ActivityLogsPostResponse +from graphiant_sdk.models.v1_adaptive_fec_put_request import V1AdaptiveFecPutRequest from graphiant_sdk.models.v1_apps_app_summary_post_request import V1AppsAppSummaryPostRequest from graphiant_sdk.models.v1_apps_app_summary_post_response import V1AppsAppSummaryPostResponse from graphiant_sdk.models.v1_apps_bandwidth_post_request import V1AppsBandwidthPostRequest @@ -2919,6 +2920,295 @@ def _v1_activity_logs_post_serialize( + @validate_call + def v1_adaptive_fec_put( + self, + authorization: Annotated[StrictStr, Field(description="Bearer token. Format: Bearer ")], + v1_adaptive_fec_put_request: V1AdaptiveFecPutRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> object: + """v1_adaptive_fec_put + + Update adaptive FEC settings for an enterprise + + :param authorization: Bearer token. Format: Bearer (required) + :type authorization: str + :param v1_adaptive_fec_put_request: (required) + :type v1_adaptive_fec_put_request: V1AdaptiveFecPutRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._v1_adaptive_fec_put_serialize( + authorization=authorization, + v1_adaptive_fec_put_request=v1_adaptive_fec_put_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "object", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def v1_adaptive_fec_put_with_http_info( + self, + authorization: Annotated[StrictStr, Field(description="Bearer token. Format: Bearer ")], + v1_adaptive_fec_put_request: V1AdaptiveFecPutRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[object]: + """v1_adaptive_fec_put + + Update adaptive FEC settings for an enterprise + + :param authorization: Bearer token. Format: Bearer (required) + :type authorization: str + :param v1_adaptive_fec_put_request: (required) + :type v1_adaptive_fec_put_request: V1AdaptiveFecPutRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._v1_adaptive_fec_put_serialize( + authorization=authorization, + v1_adaptive_fec_put_request=v1_adaptive_fec_put_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "object", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def v1_adaptive_fec_put_without_preload_content( + self, + authorization: Annotated[StrictStr, Field(description="Bearer token. Format: Bearer ")], + v1_adaptive_fec_put_request: V1AdaptiveFecPutRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """v1_adaptive_fec_put + + Update adaptive FEC settings for an enterprise + + :param authorization: Bearer token. Format: Bearer (required) + :type authorization: str + :param v1_adaptive_fec_put_request: (required) + :type v1_adaptive_fec_put_request: V1AdaptiveFecPutRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._v1_adaptive_fec_put_serialize( + authorization=authorization, + v1_adaptive_fec_put_request=v1_adaptive_fec_put_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "object", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _v1_adaptive_fec_put_serialize( + self, + authorization, + v1_adaptive_fec_put_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['Authorization'] = authorization + # process the form parameters + # process the body parameter + if v1_adaptive_fec_put_request is not None: + _body_params = v1_adaptive_fec_put_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'jwtAuth' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/v1/adaptive-fec', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + @validate_call def v1_apps_app_summary_post( self, @@ -62667,6 +62957,7 @@ def _v1_extranet_b2b_producer_review_post_serialize( def v1_extranet_b2b_services_summary_get( self, authorization: Annotated[StrictStr, Field(description="Bearer token. Format: Bearer ")], + service_type: Annotated[Optional[StrictStr], Field(description="Optional filter by branded service type")] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -62686,6 +62977,8 @@ def v1_extranet_b2b_services_summary_get( :param authorization: Bearer token. Format: Bearer (required) :type authorization: str + :param service_type: Optional filter by branded service type + :type service_type: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -62710,6 +63003,7 @@ def v1_extranet_b2b_services_summary_get( _param = self._v1_extranet_b2b_services_summary_get_serialize( authorization=authorization, + service_type=service_type, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -62734,6 +63028,7 @@ def v1_extranet_b2b_services_summary_get( def v1_extranet_b2b_services_summary_get_with_http_info( self, authorization: Annotated[StrictStr, Field(description="Bearer token. Format: Bearer ")], + service_type: Annotated[Optional[StrictStr], Field(description="Optional filter by branded service type")] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -62753,6 +63048,8 @@ def v1_extranet_b2b_services_summary_get_with_http_info( :param authorization: Bearer token. Format: Bearer (required) :type authorization: str + :param service_type: Optional filter by branded service type + :type service_type: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -62777,6 +63074,7 @@ def v1_extranet_b2b_services_summary_get_with_http_info( _param = self._v1_extranet_b2b_services_summary_get_serialize( authorization=authorization, + service_type=service_type, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -62801,6 +63099,7 @@ def v1_extranet_b2b_services_summary_get_with_http_info( def v1_extranet_b2b_services_summary_get_without_preload_content( self, authorization: Annotated[StrictStr, Field(description="Bearer token. Format: Bearer ")], + service_type: Annotated[Optional[StrictStr], Field(description="Optional filter by branded service type")] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -62820,6 +63119,8 @@ def v1_extranet_b2b_services_summary_get_without_preload_content( :param authorization: Bearer token. Format: Bearer (required) :type authorization: str + :param service_type: Optional filter by branded service type + :type service_type: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -62844,6 +63145,7 @@ def v1_extranet_b2b_services_summary_get_without_preload_content( _param = self._v1_extranet_b2b_services_summary_get_serialize( authorization=authorization, + service_type=service_type, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -62863,6 +63165,7 @@ def v1_extranet_b2b_services_summary_get_without_preload_content( def _v1_extranet_b2b_services_summary_get_serialize( self, authorization, + service_type, _request_auth, _content_type, _headers, @@ -62885,6 +63188,10 @@ def _v1_extranet_b2b_services_summary_get_serialize( # process the path parameters # process the query parameters + if service_type is not None: + + _query_params.append(('serviceType', service_type)) + # process the header parameters if authorization is not None: _header_params['Authorization'] = authorization diff --git a/graphiant_sdk/api_client.py b/graphiant_sdk/api_client.py index ebc35f08..2dc8ecf2 100644 --- a/graphiant_sdk/api_client.py +++ b/graphiant_sdk/api_client.py @@ -86,7 +86,7 @@ def __init__( self.default_headers[header_name] = header_value self.cookie = cookie # Set default User-Agent. - self.user_agent = 'OpenAPI-Generator/26.7.0/python' + self.user_agent = 'OpenAPI-Generator/26.8.0/python' self.client_side_validation = configuration.client_side_validation def __enter__(self): diff --git a/graphiant_sdk/configuration.py b/graphiant_sdk/configuration.py index 4f4f9604..10535333 100644 --- a/graphiant_sdk/configuration.py +++ b/graphiant_sdk/configuration.py @@ -563,7 +563,7 @@ def to_debug_report(self) -> str: "OS: {env}\n"\ "Python Version: {pyversion}\n"\ "Version of the API: 1.0.0\n"\ - "SDK Package Version: 26.7.0".\ + "SDK Package Version: 26.8.0".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self) -> List[HostSetting]: diff --git a/graphiant_sdk/models/__init__.py b/graphiant_sdk/models/__init__.py index e0a9003d..5dceb49f 100644 --- a/graphiant_sdk/models/__init__.py +++ b/graphiant_sdk/models/__init__.py @@ -143,6 +143,7 @@ from graphiant_sdk.models.ipfix_twamp_metrics import IpfixTwampMetrics from graphiant_sdk.models.mana_v2_aws_gateway_details_transit_connection import ManaV2AWSGatewayDetailsTransitConnection from graphiant_sdk.models.mana_v2_aws_transit_gateway_vpc import ManaV2AWSTransitGatewayVpc +from graphiant_sdk.models.mana_v2_adaptive_fec_configuration import ManaV2AdaptiveFecConfiguration from graphiant_sdk.models.mana_v2_allocation_conversion_holder import ManaV2AllocationConversionHolder from graphiant_sdk.models.mana_v2_allocation_conversion_holder_bin import ManaV2AllocationConversionHolderBin from graphiant_sdk.models.mana_v2_app import ManaV2App @@ -772,6 +773,7 @@ from graphiant_sdk.models.v1_activity_logs_post_request import V1ActivityLogsPostRequest from graphiant_sdk.models.v1_activity_logs_post_response import V1ActivityLogsPostResponse from graphiant_sdk.models.v1_activity_logs_post_response_activity_items import V1ActivityLogsPostResponseActivityItems +from graphiant_sdk.models.v1_adaptive_fec_put_request import V1AdaptiveFecPutRequest from graphiant_sdk.models.v1_apps_app_summary_post_request import V1AppsAppSummaryPostRequest from graphiant_sdk.models.v1_apps_app_summary_post_response import V1AppsAppSummaryPostResponse from graphiant_sdk.models.v1_apps_bandwidth_post_request import V1AppsBandwidthPostRequest diff --git a/graphiant_sdk/models/alertservice_zendesk_details.py b/graphiant_sdk/models/alertservice_zendesk_details.py index 4c94722a..8ef0dcf7 100644 --- a/graphiant_sdk/models/alertservice_zendesk_details.py +++ b/graphiant_sdk/models/alertservice_zendesk_details.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python @@ -27,11 +27,13 @@ class AlertserviceZendeskDetails(BaseModel): """ AlertserviceZendeskDetails """ # noqa: E501 - zendesk_api_token: StrictStr = Field(description="zendesk api token (required)", alias="zendeskApiToken", json_schema_extra={"examples": ["example string"]}) + zendesk_api_token: Optional[StrictStr] = Field(default=None, description="zendesk api token (deprecated, use zendesk_client_secret)", alias="zendeskApiToken", json_schema_extra={"examples": ["example string"]}) zendesk_assignee_id: StrictStr = Field(description="zendesk assignee id (required)", alias="zendeskAssigneeId", json_schema_extra={"examples": ["example string"]}) zendesk_base_url: StrictStr = Field(description="zendesk base url (required)", alias="zendeskBaseUrl", json_schema_extra={"examples": ["example string"]}) - zendesk_email: StrictStr = Field(description="zendesk email (required)", alias="zendeskEmail", json_schema_extra={"examples": ["example string"]}) - __properties: ClassVar[List[str]] = ["zendeskApiToken", "zendeskAssigneeId", "zendeskBaseUrl", "zendeskEmail"] + zendesk_client_id: StrictStr = Field(description="zendesk oauth client id (required)", alias="zendeskClientId", json_schema_extra={"examples": ["example string"]}) + zendesk_client_secret: StrictStr = Field(description="zendesk oauth client secret (required)", alias="zendeskClientSecret", json_schema_extra={"examples": ["example string"]}) + zendesk_email: Optional[StrictStr] = Field(default=None, description="zendesk email (deprecated, use zendesk_client_id)", alias="zendeskEmail", json_schema_extra={"examples": ["example string"]}) + __properties: ClassVar[List[str]] = ["zendeskApiToken", "zendeskAssigneeId", "zendeskBaseUrl", "zendeskClientId", "zendeskClientSecret", "zendeskEmail"] model_config = ConfigDict( validate_by_name=True, @@ -87,6 +89,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "zendeskApiToken": obj.get("zendeskApiToken"), "zendeskAssigneeId": obj.get("zendeskAssigneeId"), "zendeskBaseUrl": obj.get("zendeskBaseUrl"), + "zendeskClientId": obj.get("zendeskClientId"), + "zendeskClientSecret": obj.get("zendeskClientSecret"), "zendeskEmail": obj.get("zendeskEmail") }) return _obj diff --git a/graphiant_sdk/models/mana_v2_adaptive_fec_configuration.py b/graphiant_sdk/models/mana_v2_adaptive_fec_configuration.py new file mode 100644 index 00000000..dc1c22cd --- /dev/null +++ b/graphiant_sdk/models/mana_v2_adaptive_fec_configuration.py @@ -0,0 +1,90 @@ +# coding: utf-8 + +""" + Graphiant APIs + + Graphiant API documentation. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ManaV2AdaptiveFecConfiguration(BaseModel): + """ + ManaV2AdaptiveFecConfiguration + """ # noqa: E501 + adaptive_fec_enabled: Optional[StrictBool] = Field(default=None, alias="adaptiveFecEnabled", json_schema_extra={"examples": [True]}) + sla_classes: Optional[List[StrictStr]] = Field(default=None, alias="slaClasses") + __properties: ClassVar[List[str]] = ["adaptiveFecEnabled", "slaClasses"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ManaV2AdaptiveFecConfiguration from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ManaV2AdaptiveFecConfiguration from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "adaptiveFecEnabled": obj.get("adaptiveFecEnabled"), + "slaClasses": obj.get("slaClasses") + }) + return _obj + + diff --git a/graphiant_sdk/models/v1_adaptive_fec_put_request.py b/graphiant_sdk/models/v1_adaptive_fec_put_request.py new file mode 100644 index 00000000..02ca6a0d --- /dev/null +++ b/graphiant_sdk/models/v1_adaptive_fec_put_request.py @@ -0,0 +1,92 @@ +# coding: utf-8 + +""" + Graphiant APIs + + Graphiant API documentation. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from graphiant_sdk.models.mana_v2_adaptive_fec_configuration import ManaV2AdaptiveFecConfiguration +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class V1AdaptiveFecPutRequest(BaseModel): + """ + V1AdaptiveFecPutRequest + """ # noqa: E501 + configuration: Optional[ManaV2AdaptiveFecConfiguration] = None + __properties: ClassVar[List[str]] = ["configuration"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of V1AdaptiveFecPutRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of configuration + if self.configuration: + _dict['configuration'] = self.configuration.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of V1AdaptiveFecPutRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "configuration": ManaV2AdaptiveFecConfiguration.from_dict(obj["configuration"]) if obj.get("configuration") is not None else None + }) + return _obj + + diff --git a/graphiant_sdk/models/v1_enterprise_configuration_get_response.py b/graphiant_sdk/models/v1_enterprise_configuration_get_response.py index ae05eb3a..2cf43ba9 100644 --- a/graphiant_sdk/models/v1_enterprise_configuration_get_response.py +++ b/graphiant_sdk/models/v1_enterprise_configuration_get_response.py @@ -17,8 +17,9 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from typing import Any, ClassVar, Dict, List, Optional +from graphiant_sdk.models.mana_v2_adaptive_fec_configuration import ManaV2AdaptiveFecConfiguration from graphiant_sdk.models.mana_v2_enterprise_configuration import ManaV2EnterpriseConfiguration from typing import Optional, Set from typing_extensions import Self @@ -28,8 +29,9 @@ class V1EnterpriseConfigurationGetResponse(BaseModel): """ V1EnterpriseConfigurationGetResponse """ # noqa: E501 + adaptive_fec_config: Optional[ManaV2AdaptiveFecConfiguration] = Field(default=None, alias="adaptiveFecConfig") configuration: Optional[ManaV2EnterpriseConfiguration] = None - __properties: ClassVar[List[str]] = ["configuration"] + __properties: ClassVar[List[str]] = ["adaptiveFecConfig", "configuration"] model_config = ConfigDict( validate_by_name=True, @@ -70,6 +72,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of adaptive_fec_config + if self.adaptive_fec_config: + _dict['adaptiveFecConfig'] = self.adaptive_fec_config.to_dict() # override the default output from pydantic by calling `to_dict()` of configuration if self.configuration: _dict['configuration'] = self.configuration.to_dict() @@ -85,6 +90,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ + "adaptiveFecConfig": ManaV2AdaptiveFecConfiguration.from_dict(obj["adaptiveFecConfig"]) if obj.get("adaptiveFecConfig") is not None else None, "configuration": ManaV2EnterpriseConfiguration.from_dict(obj["configuration"]) if obj.get("configuration") is not None else None }) return _obj diff --git a/pyproject.toml b/pyproject.toml index 487da27d..3bbdd925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "graphiant_sdk" -version = "26.7.0" +version = "26.8.0" description = "Graphiant APIs" authors = [ {name = "OpenAPI Generator Community",email = "team@openapitools.org"}, diff --git a/scripts/generate.sh b/scripts/generate.sh index 5ef90a60..3bcd5a62 100755 --- a/scripts/generate.sh +++ b/scripts/generate.sh @@ -8,7 +8,7 @@ # # Usage: # bash scripts/generate.sh # uses api/openapi.yaml -# OPENAPI_SPEC=api/graphiant_api_docs_v26.7.0.json bash scripts/generate.sh +# OPENAPI_SPEC=api/graphiant_api_docs_v26.8.0.json bash scripts/generate.sh set -euo pipefail diff --git a/scripts/oasdiff.sh b/scripts/oasdiff.sh new file mode 100755 index 00000000..b30b6de6 --- /dev/null +++ b/scripts/oasdiff.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Compare two versions of the OpenAPI spec with oasdiff (breaking-change check or full changelog). +# +# Requires Docker. +# +# Usage: +# bash scripts/oasdiff.sh # breaking-change report: prev spec vs latest spec +# MODE=changelog bash scripts/oasdiff.sh # full markdown changelog instead +# bash scripts/oasdiff.sh api/old.json api/new.json # diff two explicit spec files +# +# Auto-detection (no args): +# - If two or more api/graphiant_api_docs_v*.json files are present locally (e.g. you just +# dropped a new spec in alongside the old one), diffs the two highest versions. +# - Otherwise diffs the latest local spec against the version committed on $BASE_REF +# (default: origin/main). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +MODE="${MODE:-breaking}" +BASE_REF="${BASE_REF:-origin/main}" +CLEANUP="" + +if [ "$#" -eq 2 ]; then + BASE_SPEC="$1" + HEAD_SPEC="$2" +else + HEAD_SPEC="$(ls api/graphiant_api_docs_v*.json 2>/dev/null | sort -V | tail -1 || true)" + if [ -z "${HEAD_SPEC:-}" ]; then + echo "❌ No api/graphiant_api_docs_v*.json found." >&2 + exit 1 + fi + + LOCAL_SPEC_COUNT="$(ls api/graphiant_api_docs_v*.json 2>/dev/null | wc -l | tr -d ' ')" + if [ "$LOCAL_SPEC_COUNT" -ge 2 ]; then + BASE_SPEC="$(ls api/graphiant_api_docs_v*.json | sort -V | tail -2 | head -1)" + else + BASE_SPEC_NAME="$(git ls-tree -r --name-only "$BASE_REF" -- api 2>/dev/null | grep -E '^api/graphiant_api_docs_v[0-9.]+\.json$' | sort -V | tail -1 || true)" + if [ -z "$BASE_SPEC_NAME" ]; then + echo "❌ Could not find a previous spec on ${BASE_REF} to diff against. Pass two spec files explicitly." >&2 + exit 1 + fi + BASE_SPEC="api/.oasdiff-base.json" + git show "${BASE_REF}:${BASE_SPEC_NAME}" > "$BASE_SPEC" + CLEANUP="$BASE_SPEC" + fi +fi + +[ -n "$CLEANUP" ] && trap 'rm -f "$CLEANUP"' EXIT + +if ! command -v docker &>/dev/null; then + echo "❌ Docker not found. oasdiff runs via the tufin/oasdiff Docker image: https://github.com/oasdiff/oasdiff" >&2 + exit 1 +fi + +FORMAT="text" +[ "$MODE" = "changelog" ] && FORMAT="markdown" + +echo "🔍 oasdiff ${MODE}: ${BASE_SPEC} → ${HEAD_SPEC}" +echo +docker run --rm -t --mount "type=bind,src=${REPO_ROOT},dst=/data" tufin/oasdiff "${MODE}" \ + --format "${FORMAT}" \ + "/data/${BASE_SPEC}" \ + "/data/${HEAD_SPEC}" diff --git a/test/test_mana_v2_adaptive_fec_configuration.py b/test/test_mana_v2_adaptive_fec_configuration.py new file mode 100644 index 00000000..5207f93d --- /dev/null +++ b/test/test_mana_v2_adaptive_fec_configuration.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Graphiant APIs + + Graphiant API documentation. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from graphiant_sdk.models.mana_v2_adaptive_fec_configuration import ManaV2AdaptiveFecConfiguration + +class TestManaV2AdaptiveFecConfiguration(unittest.TestCase): + """ManaV2AdaptiveFecConfiguration unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ManaV2AdaptiveFecConfiguration: + """Test ManaV2AdaptiveFecConfiguration + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ManaV2AdaptiveFecConfiguration` + """ + model = ManaV2AdaptiveFecConfiguration() + if include_optional: + return ManaV2AdaptiveFecConfiguration( + adaptive_fec_enabled = True, + sla_classes = [ + 'ENUM_VALUE' + ] + ) + else: + return ManaV2AdaptiveFecConfiguration( + ) + """ + + def testManaV2AdaptiveFecConfiguration(self): + """Test ManaV2AdaptiveFecConfiguration""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_v1_adaptive_fec_put_request.py b/test/test_v1_adaptive_fec_put_request.py new file mode 100644 index 00000000..bacef617 --- /dev/null +++ b/test/test_v1_adaptive_fec_put_request.py @@ -0,0 +1,55 @@ +# coding: utf-8 + +""" + Graphiant APIs + + Graphiant API documentation. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from graphiant_sdk.models.v1_adaptive_fec_put_request import V1AdaptiveFecPutRequest + +class TestV1AdaptiveFecPutRequest(unittest.TestCase): + """V1AdaptiveFecPutRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> V1AdaptiveFecPutRequest: + """Test V1AdaptiveFecPutRequest + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `V1AdaptiveFecPutRequest` + """ + model = V1AdaptiveFecPutRequest() + if include_optional: + return V1AdaptiveFecPutRequest( + configuration = graphiant_sdk.models.mana_v2_adaptive_fec_configuration.manaV2AdaptiveFecConfiguration( + adaptive_fec_enabled = True, + sla_classes = [ + 'ENUM_VALUE' + ], ) + ) + else: + return V1AdaptiveFecPutRequest( + ) + """ + + def testV1AdaptiveFecPutRequest(self): + """Test V1AdaptiveFecPutRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main()