diff --git a/.github/workflows/fastpix-python.yml b/.github/workflows/fastpix-python.yml
index 4e91d61..8848e2f 100644
--- a/.github/workflows/fastpix-python.yml
+++ b/.github/workflows/fastpix-python.yml
@@ -31,7 +31,7 @@ jobs:
python-version: '3.x'
- name: Install build tools
- run: python -m pip install --upgrade build twine
+ run: python -m pip install build==1.6.0 twine==7.0.0
- name: Build package
run: python -m build
diff --git a/.gitignore b/.gitignore
index f1fbcd8..d6bc7a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,22 +142,15 @@ dmypy.json
*.backup
*.bak
-# Validation harness — artifacts contain workspace IDs and live-stream secrets
-# (streamKey, srtSecret); generated reports change every run.
+# Test harness output (generated per run)
.venv-tests/
tests/artifacts/
tests/artifacts_non_get/
-tests/*OPENAPI_RESPONSE*.md
-tests/BROKEN_LINKS_REPORT.md
-tests/NON_GET_ENDPOINTS_VALIDATION_REPORT.md
-tests/GET_ENDPOINTS_VALIDATION_REPORT.md
-
-# Local SDK usage examples (contain live workspace credentials/IDs)
+tests/*_REPORT.md
+tests/*_FIX_SUGGESTIONS.md
tests/examples/
node_modules/
-#Local files
-/fixed.yaml
-/fastpix-openai.yaml
-fastpix.yaml
\ No newline at end of file
+# Local OpenAPI spec snapshot used by the test harness
+/openapi.yaml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c13cd09..76bab24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,45 @@
All notable changes to this project will be documented in this file.
+---
+
+## [1.2.0]
+
+### Breaking
+
+- **Media `duration` is now a float (seconds)** instead of an `"HH:MM:SS"`
+ string, matching the updated API. Affects the media responses returned by
+ `get_media`, `list_media`, `list_live_clips`, `updated_media`,
+ `updated_source_access`, `updated_mp4_support`, `get_media_clips`, and the
+ playlist `mediaList` items.
+
+### Added
+
+- **`enable_recording`** on live stream creation (`inputMediaSettings`,
+ defaults to `true`) — controls whether the livestream is recorded to a VOD
+ asset.
+- **`access_restrictions`** (domain and user-agent allow/deny policies) on
+ live playback ID create/get and on live stream `playbackSettings`.
+- **`update_live_stream_domain_restrictions()` /
+ `update_live_stream_user_agent_restrictions()`** (and `_async` variants) —
+ `PATCH /live/streams/{streamId}/playback-ids/{playbackId}/domains` and
+ `/user-agents`.
+- **Async variants** for the on-demand `update_domain_restrictions` and
+ `update_user_agent_restrictions`, which were sync-only.
+- **Model contract test suite** (`tests/test_models.py`).
+
+### Fixed
+
+- **Async error handling never raised** — `_raise_for_status_async` was called
+ without `await` across the async methods, so failed responses returned
+ `None` instead of raising typed errors.
+- **Return annotations for live playback ID create/get, `list_media`, and
+ `list_live_clips`** corrected to the actual response envelopes
+ (`PlaybackIDSuccessResponse`, `ListMediaResponse`, `ListLiveClipsResponse`).
+- **Return annotations across all resource methods** — 104 methods declared the
+ inner `data` type or a list where the SDK actually returns the `{success, data}`
+ envelope, so type checkers rejected `.data` access. All now match the returned
+ class; a test enforces this.
---
@@ -57,6 +96,7 @@ All notable changes to this project will be documented in this file.
## [1.1.4]
### Changed
+
- **SDK version bump: `1.1.3` → `1.1.4`.**
A maintenance release that aligns the SDK's internal version identifiers and
applies behaviour-preserving code-quality cleanup. It contains no functional,
@@ -76,6 +116,7 @@ All notable changes to this project will be documented in this file.
conditionals merged. No public-surface impact.
### Compatibility
+
- No changes to public types, method signatures, request/response models,
default server URLs, hooks, or retry logic.
- No action required for existing integrations — upgrade the dependency and
@@ -89,14 +130,14 @@ All notable changes to this project will be documented in this file.
All FastPix-owned hosts, API endpoints, and documentation links are being moved from the `.io` TLD to `.com`. The `.io` hosts continue to serve traffic during the transition window, **but they are slated for deprecation soon** — please update any hard-coded references in your application as part of your next deploy.
-| Old (`.io`) | New (`.com`) |
-|---|---|
-| `api.fastpix.io` | `api.fastpix.com` |
-| `stream.fastpix.io` | `stream.fastpix.com` |
-| `images.fastpix.io` | `images.fastpix.com` |
+| Old (`.io`) | New (`.com`) |
+| ---------------------- | ----------------------- |
+| `api.fastpix.io` | `api.fastpix.com` |
+| `stream.fastpix.io` | `stream.fastpix.com` |
+| `images.fastpix.io` | `images.fastpix.com` |
| `dashboard.fastpix.io` | `dashboard.fastpix.com` |
-| `www.fastpix.io` | `www.fastpix.com` |
-| `docs.fastpix.io/...` | `fastpix.com/docs/...` |
+| `www.fastpix.io` | `www.fastpix.com` |
+| `docs.fastpix.io/...` | `fastpix.com/docs/...` |
What this means for users of `fastpix_python`:
@@ -120,6 +161,7 @@ What this means for users of `fastpix_python`:
## [1.1.2]
### Fixed
+
- Fixed `events` field in `get_video_view_details` response returning empty objects — added `validation_alias` mappings for abbreviated API keys (`pt`, `e`, `vt`, `d`) to full camelCase names (`playerPlayheadTime`, `eventName`, `viewerTime`, `eventDetails`)
- Fixed `eventDetails` nested object returning raw abbreviated keys — introduced `EventDetails` model with proper field mappings (`host`→`hostName`, `txt`→`text`, `c`→`code`, `err`→`error`, `t`→`type`, `u`→`url`, `br`→`bitrate`, `h`→`height`, `fps`→`fps`, `cd`→`codec`, `w`→`width`)
- Fixed `fpSDK` and `fpSDKVersion` fields missing from response — added `AliasChoices` to accept both `fpSdk` and `fpSDK` variants from the API
@@ -127,6 +169,7 @@ What this means for users of `fastpix_python`:
- Added missing `custom` field to `Views` model to capture user-defined metadata object
### Improved
+
- Response models for video view details now fully conform to the OpenAPI spec field names
---
@@ -134,37 +177,42 @@ What this means for users of `fastpix_python`:
## [1.1.1]
### Fixed
+
- Fixed SDK import paths in `_sub_sdk_map` - changed from `Fastpix.*` to `fastpix_python.*` to resolve `ModuleNotFoundError` for end users
- Fixed all documentation examples - removed unnecessary `sys.path.append()` statements
- Updated method name from `create_from_url` to `create_media` in examples
### Improved
+
- All SDK documentation examples now work out-of-the-box without workarounds
- Consistent import statements across all documentation files
## [1.1.0]
### Fixed
+
- Fixed missing parameters in multiple API methods.
### Improved
+
- Improved overall developer experience through more accurate typings.
## [1.0.3]
### Fixed
-- Fixed pyproject.toml file Packaging Issue
+- Fixed pyproject.toml file Packaging Issue
## [1.0.2]
### Fixed
-- Fixed Packaging Issue
+- Fixed Packaging Issue
## [1.0.1]
### Fixed
+
- Fixed all error handling links in README.md documentation
- Corrected typos in file paths (e.g., `fFastpix` → `Fastpix`)
- Updated filenames to match actual error class files (added missing underscores)
@@ -174,6 +222,7 @@ What this means for users of `fastpix_python`:
## [1.0.0]
### Added
+
- Complete API coverage for Media, Live Streaming, Video Data, and Signing Keys
- Python 3.9+ support with async/await patterns and type hints
- Media upload, management, and processing capabilities
@@ -188,12 +237,14 @@ What this means for users of `fastpix_python`:
- Built-in retry mechanisms and timeout handling
### Changed
+
- Reorganized package structure for better maintainability
- Updated dependencies to modern Python packages (httpx, pydantic, httpcore)
- Improved API design with better error handling
- Enhanced documentation and examples
### Fixed
+
- Improved error handling with specific exception types
- Fixed type annotation issues for better IDE support
- Ensured consistent API patterns across modules
@@ -203,10 +254,12 @@ What this means for users of `fastpix_python`:
## [0.1.8]
### Added
+
- Enhanced README documentation with comprehensive usage examples
- Improved project setup and installation instructions
### Changed
+
- Updated version number to reflect latest improvements
- Restructured documentation for better user experience
- Enhanced code examples and API usage guides
@@ -216,10 +269,12 @@ What this means for users of `fastpix_python`:
## [0.1.7]
### Added
+
- New base URL configuration system for better API connectivity
- Support for different API environments (production, staging, development)
### Changed
+
- Updated base URL configuration for improved API endpoint resolution
- Enhanced connection stability and reliability
- Improved error handling for connection issues
@@ -229,10 +284,12 @@ What this means for users of `fastpix_python`:
## [0.1.6]
### Added
+
- Project URL management system for better package distribution
- Enhanced package metadata and configuration
### Changed
+
- Updated project URLs in configuration files for better package identification
- Improved package metadata and distribution information
- Enhanced project discoverability and documentation links
@@ -242,11 +299,13 @@ What this means for users of `fastpix_python`:
## [0.1.5]
### Added
+
- Comprehensive version tracking and file management system
- Automated version control and release management
- Initial project structure and configuration framework
### Changed
+
- Updated version number and project configuration
- Improved project organization and file structure
- Enhanced build and deployment processes
@@ -256,10 +315,12 @@ What this means for users of `fastpix_python`:
## [0.1.4]
### Added
+
- New package naming convention for better identification
- Enhanced package metadata and distribution information
### Changed
+
- Changed package name for better identification and distribution
- Updated package metadata and configuration
- Improved package discoverability and installation process
@@ -269,10 +330,12 @@ What this means for users of `fastpix_python`:
## [0.1.3]
### Added
+
- Version management improvements
- Enhanced configuration system
### Changed
+
- Updated version number to reflect latest changes
- Improved project configuration and build processes
- Enhanced package metadata and dependencies
@@ -282,15 +345,18 @@ What this means for users of `fastpix_python`:
## [0.1.2]
### Added
+
- Comprehensive documentation link validation system
- Enhanced workflow automation and CI/CD pipeline
### Fixed
+
- Corrected redirection links in README documentation
- Fixed broken documentation links for better user experience
- Resolved navigation issues in project documentation
### Changed
+
- Updated workflow configuration and processes
- Improved project automation and deployment pipeline
- Enhanced documentation structure and organization
@@ -300,10 +366,12 @@ What this means for users of `fastpix_python`:
## [0.1.1]
### Changed
+
- Updated codebase with consistent naming conventions
- Added comprehensive package description
### Fixed
+
- Resolved naming convention inconsistencies
---
@@ -311,6 +379,7 @@ What this means for users of `fastpix_python`:
## [0.1.0]
### Added
+
- Initial release of FastPix Python SDK
- Sync and async client support
- Media API integration with upload, management, and processing
@@ -320,4 +389,4 @@ What this means for users of `fastpix_python`:
- Livestream API integration
- Livestream management (create, update, delete)
- Playback ID management for livestreams
-- Simulcast configuration for livestreams
\ No newline at end of file
+- Simulcast configuration for livestreams
diff --git a/README.md b/README.md
index 3ad23cf..fafaeea 100644
--- a/README.md
+++ b/README.md
@@ -500,6 +500,8 @@ For detailed documentation, see [FastPix Live Stream Overview](https://fastpix.c
- [Create Playback ID](https://github.com/FastPix/fastpix-python/blob/feature/fixed-missing-parameters/docs/sdks/liveplayback/README.md#create_playback_id) - Generate secure live playback access
- [Delete Playback ID](https://github.com/FastPix/fastpix-python/blob/feature/fixed-missing-parameters/docs/sdks/liveplayback/README.md#delete_playback_id) - Revoke live playback access
- [Get Playback ID](https://github.com/FastPix/fastpix-python/blob/feature/fixed-missing-parameters/docs/sdks/liveplayback/README.md#get_playback_id_details) - Retrieve live playback configuration
+- [Update Domain Restrictions](https://github.com/FastPix/fastpix-python/blob/feature/fixed-missing-parameters/docs/sdks/liveplayback/README.md#update_live_stream_domain_restrictions) - Configure domain-based access control
+- [Update User-Agent Restrictions](https://github.com/FastPix/fastpix-python/blob/feature/fixed-missing-parameters/docs/sdks/liveplayback/README.md#update_live_stream_user_agent_restrictions) - Configure user-agent-based access control
#### Simulcast Stream
- [Create Simulcast](https://github.com/FastPix/fastpix-python/blob/feature/fixed-missing-parameters/docs/sdks/simulcaststream/README.md#create) - Set up multi-platform streaming
diff --git a/docs/models/getallmediaresponse.md b/docs/models/getallmediaresponse.md
index 9bdc4a6..67ece7a 100644
--- a/docs/models/getallmediaresponse.md
+++ b/docs/models/getallmediaresponse.md
@@ -28,7 +28,7 @@
| `moderation` | [Optional[models.AiResponseRecord]](../models/airesponserecord.md) | :heavy_minus_sign: | Represents an AI response record containing status and data for AI-generated features like summary, chapters, named entities, or moderation. | |
| `is_audio_only` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether the media contains only audio (no video track). | false |
| `subtitle_available` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether subtitles are available for the media. | true |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | The length of the media in seconds, with a maximum allowed duration of 12 hours per individual media. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `frame_rate` | *Optional[str]* | :heavy_minus_sign: | Frame rate quantifies the speed at which frames are displayed per second. It represents the range of frames available for a specific track. The indeterminable frame rate of the input file is indicated by a value of -1. | 30/1 |
| `aspect_ratio` | *OptionalNullable[str]* | :heavy_minus_sign: | The aspect ratio of a video is a value that describes the relative shape of a video based on its width and height. | 16:9 |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was created, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
diff --git a/docs/models/getmediaresponse.md b/docs/models/getmediaresponse.md
index ad461f5..35007c2 100644
--- a/docs/models/getmediaresponse.md
+++ b/docs/models/getmediaresponse.md
@@ -28,7 +28,7 @@
| `moderation` | [Optional[models.AiResponseRecord]](../models/airesponserecord.md) | :heavy_minus_sign: | Represents an AI response record containing status and data for AI-generated features like summary, chapters, named entities, or moderation. | |
| `is_audio_only` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether the media contains only audio (no video track). | false |
| `subtitle_available` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether subtitles are available for the media. | true |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | The length of the media in seconds, with a maximum allowed duration of 12 hours per individual media. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `frame_rate` | *Optional[str]* | :heavy_minus_sign: | Frame rate quantifies the speed at which frames are displayed per second. It represents the range of frames available for a specific track. The indeterminable frame rate of the input file is indicated by a value of -1. | 30/1 |
| `aspect_ratio` | *OptionalNullable[str]* | :heavy_minus_sign: | The aspect ratio of a video is a value that describes the relative shape of a video based on its width and height. | 16:9 |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was created, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
diff --git a/docs/models/inputmediasettings.md b/docs/models/inputmediasettings.md
index 6292acd..87da3d6 100644
--- a/docs/models/inputmediasettings.md
+++ b/docs/models/inputmediasettings.md
@@ -11,4 +11,5 @@ Contains configuration details for input media settings.
| `reconnect_window` | *Optional[int]* | :heavy_minus_sign: | Time period (in seconds) FastPix waits to reconnect before ending the stream when disconnected.
| 60 |
| `media_policy` | [Optional[models.BasicAccessPolicy]](../models/basicaccesspolicy.md) | :heavy_minus_sign: | Basic access policy for media content | |
| `metadata` | Dict[str, *str*] | :heavy_minus_sign: | Custom key–value pairs for tagging livestreams.
Allows up to 10 entries with a maximum of 255 characters each.
| {
"livestream_name": "your-livestream-name"
} |
-| `enable_dvr_mode` | *Optional[bool]* | :heavy_minus_sign: | Enables DVR (Digital Video Recorder) functionality, allowing viewers to pause, rewind, and resume live playback.
| |
\ No newline at end of file
+| `enable_dvr_mode` | *Optional[bool]* | :heavy_minus_sign: | Enables DVR (Digital Video Recorder) functionality, allowing viewers to pause, rewind, and resume live playback.
| |
+| `enable_recording` | *Optional[bool]* | :heavy_minus_sign: | Controls whether the livestream is recorded to a VOD asset (Live-to-VOD). When set to true (default), FastPix records and stores the livestream for on-demand viewing. When set to false, the livestream is not recorded.
| true |
\ No newline at end of file
diff --git a/docs/models/livemediaclips.md b/docs/models/livemediaclips.md
index e26189c..2157824 100644
--- a/docs/models/livemediaclips.md
+++ b/docs/models/livemediaclips.md
@@ -22,7 +22,7 @@
| `generated_subtitles` | List[[models.TracksSubtitles](../models/trackssubtitles.md)] | :heavy_minus_sign: | List of generated subtitle tracks associated with the media. | |
| `is_audio_only` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether the media contains only audio (no video track). | false |
| `subtitle_available` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether subtitles are available for the media. | true |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | The length of the media in seconds, with a maximum allowed duration of 12 hours per individual media. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `aspect_ratio` | *OptionalNullable[str]* | :heavy_minus_sign: | The aspect ratio of a video is a value that describes the relative shape of a video based on its width and height. | 16:9 |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was created, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
| `updated_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was updated, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
\ No newline at end of file
diff --git a/docs/models/media.md b/docs/models/media.md
index ec2bbb7..03e5379 100644
--- a/docs/models/media.md
+++ b/docs/models/media.md
@@ -29,7 +29,7 @@
| `is_audio_only` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether the media contains only audio (no video track). | false |
| `subtitle_available` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether subtitles are available for the media. | true |
| `optimize_audio` | *Optional[bool]* | :heavy_minus_sign: | Enhance the quality and volume of the audio track. This is available for pre-recorded content only.
| false |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | The length of the media in seconds, with a maximum allowed duration of 12 hours per individual media. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `aspect_ratio` | *OptionalNullable[str]* | :heavy_minus_sign: | The aspect ratio of a video is a value that describes the relative shape of a video based on its width and height. | 16:9 |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was created, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
| `updated_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was updated, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
\ No newline at end of file
diff --git a/docs/models/mediaclipresponsedata.md b/docs/models/mediaclipresponsedata.md
index ecff943..db4c4c9 100644
--- a/docs/models/mediaclipresponsedata.md
+++ b/docs/models/mediaclipresponsedata.md
@@ -6,7 +6,7 @@
| Field | Type | Required | Description | Example |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `id` | *Optional[str]* | :heavy_minus_sign: | The unique identifier assigned to the media by FastPix. | your-media-id |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | Duration of the media in HH:MM:SS format. | 00:00:13 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `status` | [Optional[models.MediaClipResponseStatus]](../models/mediaclipresponsestatus.md) | :heavy_minus_sign: | The current processing status of the media. | Ready |
| `thumbnail` | *Optional[str]* | :heavy_minus_sign: | A video thumbnail that acts as a preview image for the video. | https://images.fastpix.app/your-media-id/thumbnail.png |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Timestamp of when the media was created. | 2025-03-12T06:17:26.403017Z |
diff --git a/docs/models/playbackidrequest.md b/docs/models/playbackidrequest.md
index 9dfcda4..191a4a5 100644
--- a/docs/models/playbackidrequest.md
+++ b/docs/models/playbackidrequest.md
@@ -5,4 +5,5 @@
| Field | Type | Required | Description |
| -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
-| `access_policy` | [Optional[models.BasicAccessPolicy]](../models/basicaccesspolicy.md) | :heavy_minus_sign: | Basic access policy for media content |
\ No newline at end of file
+| `access_policy` | [Optional[models.BasicAccessPolicy]](../models/basicaccesspolicy.md) | :heavy_minus_sign: | Basic access policy for media content |
+| `access_restrictions` | [Optional[models.PlaybackIDAccessRestrictions]](../models/playbackidaccessrestrictions.md) | :heavy_minus_sign: | Optional domain and user-agent access restrictions applied to the playback ID. |
\ No newline at end of file
diff --git a/docs/models/playbackidsuccessresponsedata.md b/docs/models/playbackidsuccessresponsedata.md
index 18c43e1..334af8d 100644
--- a/docs/models/playbackidsuccessresponsedata.md
+++ b/docs/models/playbackidsuccessresponsedata.md
@@ -6,4 +6,5 @@
| Field | Type | Required | Description | Example |
| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `id` | *Optional[str]* | :heavy_minus_sign: | Unique identifier for the playbackId | your-playback-id |
-| `access_policy` | *Optional[str]* | :heavy_minus_sign: | Determines if access to the streamed content is kept private or available to all. | public |
\ No newline at end of file
+| `access_policy` | *Optional[str]* | :heavy_minus_sign: | Determines if access to the streamed content is kept private or available to all. | public |
+| `access_restrictions` | [Optional[models.PlaybackIDAccessRestrictions]](../models/playbackidaccessrestrictions.md) | :heavy_minus_sign: | Optional domain and user-agent access restrictions applied to the playback ID. | |
\ No newline at end of file
diff --git a/docs/models/playbacksettings.md b/docs/models/playbacksettings.md
index e371c8d..a4ee403 100644
--- a/docs/models/playbacksettings.md
+++ b/docs/models/playbacksettings.md
@@ -7,4 +7,5 @@ Displays the result of the playback settings.
| Field | Type | Required | Description |
| -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
-| `access_policy` | [Optional[models.BasicAccessPolicy]](../models/basicaccesspolicy.md) | :heavy_minus_sign: | Basic access policy for media content |
\ No newline at end of file
+| `access_policy` | [Optional[models.BasicAccessPolicy]](../models/basicaccesspolicy.md) | :heavy_minus_sign: | Basic access policy for media content |
+| `access_restrictions` | [Optional[models.PlaybackIDAccessRestrictions]](../models/playbackidaccessrestrictions.md) | :heavy_minus_sign: | Optional domain and user-agent access restrictions applied to the playback ID. |
\ No newline at end of file
diff --git a/docs/models/playlistbyidresponsemedialistitem.md b/docs/models/playlistbyidresponsemedialistitem.md
index 3a389f5..adf3839 100644
--- a/docs/models/playlistbyidresponsemedialistitem.md
+++ b/docs/models/playlistbyidresponsemedialistitem.md
@@ -7,7 +7,7 @@
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Timestamp of media creation in the workspace. | 2025-03-21T05:58:38.000708Z |
| `creator_id` | *OptionalNullable[str]* | :heavy_minus_sign: | Creator ID of the media. | FastPix@14612 |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | Duration of the media in hh:mm:ss format. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `id` | *Optional[str]* | :heavy_minus_sign: | unique id of the particular media. | your-playlist-id |
| `source_resolution` | *Optional[str]* | :heavy_minus_sign: | source resolution of the media. | 1080p |
| `status` | *Optional[str]* | :heavy_minus_sign: | status of the media, only media with ready status is added to playlist. | Ready |
diff --git a/docs/models/sourceaccessmedia.md b/docs/models/sourceaccessmedia.md
index 54bebf5..3627fae 100644
--- a/docs/models/sourceaccessmedia.md
+++ b/docs/models/sourceaccessmedia.md
@@ -26,7 +26,7 @@
| `moderation` | [Optional[models.AiResponseRecord]](../models/airesponserecord.md) | :heavy_minus_sign: | Represents an AI response record containing status and data for AI-generated features like summary, chapters, named entities, or moderation. | |
| `is_audio_only` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether the media contains only audio (no video track). | false |
| `subtitle_available` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether subtitles are available for the media. | true |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | The length of the media in seconds, with a maximum allowed duration of 12 hours per individual media. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `aspect_ratio` | *OptionalNullable[str]* | :heavy_minus_sign: | The aspect ratio of a video describes its shape based on the relationship between its width and height. | 16:9 |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was created, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
| `updated_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was updated, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamdomainrestrictionsdata.md b/docs/models/updatelivestreamdomainrestrictionsdata.md
new file mode 100644
index 0000000..77dfffc
--- /dev/null
+++ b/docs/models/updatelivestreamdomainrestrictionsdata.md
@@ -0,0 +1,10 @@
+# UpdateLiveStreamDomainRestrictionsData
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
+| `default_policy` | *Optional[str]* | :heavy_minus_sign: | Specify the fallback behavior for domains that are not listed in the allow or deny lists. | allow |
+| `allow` | List[*str*] | :heavy_minus_sign: | List of domains explicitly allowed to play the stream. | [
"yourdomain.com",
"yourworkdomain.com"
] |
+| `deny` | List[*str*] | :heavy_minus_sign: | List of domains explicitly denied from accessing the stream. | [
"sampledomain.com"
] |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamdomainrestrictionsdefaultpolicy.md b/docs/models/updatelivestreamdomainrestrictionsdefaultpolicy.md
new file mode 100644
index 0000000..ae7ef76
--- /dev/null
+++ b/docs/models/updatelivestreamdomainrestrictionsdefaultpolicy.md
@@ -0,0 +1,11 @@
+# UpdateLiveStreamDomainRestrictionsDefaultPolicy
+
+Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists.
+
+
+## Values
+
+| Name | Value |
+| ------- | ------- |
+| `ALLOW` | allow |
+| `DENY` | deny |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamdomainrestrictionsrequest.md b/docs/models/updatelivestreamdomainrestrictionsrequest.md
new file mode 100644
index 0000000..8a29936
--- /dev/null
+++ b/docs/models/updatelivestreamdomainrestrictionsrequest.md
@@ -0,0 +1,10 @@
+# UpdateLiveStreamDomainRestrictionsRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
+| `stream_id` | *str* | :heavy_check_mark: | N/A | your-stream-id |
+| `playback_id` | *str* | :heavy_check_mark: | N/A | your-playback-id |
+| `body` | [models.UpdateLiveStreamDomainRestrictionsRequestBody](../models/updatelivestreamdomainrestrictionsrequestbody.md) | :heavy_check_mark: | N/A | |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamdomainrestrictionsrequestbody.md b/docs/models/updatelivestreamdomainrestrictionsrequestbody.md
new file mode 100644
index 0000000..a09e7e8
--- /dev/null
+++ b/docs/models/updatelivestreamdomainrestrictionsrequestbody.md
@@ -0,0 +1,10 @@
+# UpdateLiveStreamDomainRestrictionsRequestBody
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
+| `default_policy` | [Optional[models.UpdateLiveStreamDomainRestrictionsDefaultPolicy]](../models/updatelivestreamdomainrestrictionsdefaultpolicy.md) | :heavy_minus_sign: | Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists. | allow |
+| `allow` | List[*str*] | :heavy_minus_sign: | List of domains explicitly allowed to play the stream. | [
"yourdomain.com",
"sampledomain.com"
] |
+| `deny` | List[*str*] | :heavy_minus_sign: | List of domains explicitly denied from accessing the stream. | [
"yourworkdomain.com"
] |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamdomainrestrictionsresponsebody.md b/docs/models/updatelivestreamdomainrestrictionsresponsebody.md
new file mode 100644
index 0000000..ea17bb9
--- /dev/null
+++ b/docs/models/updatelivestreamdomainrestrictionsresponsebody.md
@@ -0,0 +1,11 @@
+# UpdateLiveStreamDomainRestrictionsResponseBody
+
+Successfully updated domain restrictions
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
+| `success` | *Optional[bool]* | :heavy_minus_sign: | Shows the request status. Returns true for success and false for failure. | true |
+| `data` | [Optional[models.UpdateLiveStreamDomainRestrictionsData]](../models/updatelivestreamdomainrestrictionsdata.md) | :heavy_minus_sign: | N/A | |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamuseragentrestrictionsdata.md b/docs/models/updatelivestreamuseragentrestrictionsdata.md
new file mode 100644
index 0000000..40bc61a
--- /dev/null
+++ b/docs/models/updatelivestreamuseragentrestrictionsdata.md
@@ -0,0 +1,10 @@
+# UpdateLiveStreamUserAgentRestrictionsData
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| `default_policy` | *Optional[str]* | :heavy_minus_sign: | Specifies the default behavior for user agents not listed in the allow or deny lists. | allow |
+| `allow` | List[*str*] | :heavy_minus_sign: | List of user-agent substrings explicitly allowed. | [
"Mozilla/55.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
] |
+| `deny` | List[*str*] | :heavy_minus_sign: | List of user-agent substrings explicitly denied. | [
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/53745.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36"
] |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamuseragentrestrictionsdefaultpolicy.md b/docs/models/updatelivestreamuseragentrestrictionsdefaultpolicy.md
new file mode 100644
index 0000000..5e8bdbb
--- /dev/null
+++ b/docs/models/updatelivestreamuseragentrestrictionsdefaultpolicy.md
@@ -0,0 +1,11 @@
+# UpdateLiveStreamUserAgentRestrictionsDefaultPolicy
+
+The default behavior when a user-agent is not listed in `allow` or `deny`.
+
+
+## Values
+
+| Name | Value |
+| ------- | ------- |
+| `ALLOW` | allow |
+| `DENY` | deny |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamuseragentrestrictionsrequest.md b/docs/models/updatelivestreamuseragentrestrictionsrequest.md
new file mode 100644
index 0000000..9224b24
--- /dev/null
+++ b/docs/models/updatelivestreamuseragentrestrictionsrequest.md
@@ -0,0 +1,10 @@
+# UpdateLiveStreamUserAgentRestrictionsRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| `stream_id` | *str* | :heavy_check_mark: | N/A | your-stream-id |
+| `playback_id` | *str* | :heavy_check_mark: | N/A | your-playback-id |
+| `body` | [models.UpdateLiveStreamUserAgentRestrictionsRequestBody](../models/updatelivestreamuseragentrestrictionsrequestbody.md) | :heavy_check_mark: | N/A | |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamuseragentrestrictionsrequestbody.md b/docs/models/updatelivestreamuseragentrestrictionsrequestbody.md
new file mode 100644
index 0000000..0f3cf90
--- /dev/null
+++ b/docs/models/updatelivestreamuseragentrestrictionsrequestbody.md
@@ -0,0 +1,10 @@
+# UpdateLiveStreamUserAgentRestrictionsRequestBody
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| `default_policy` | [Optional[models.UpdateLiveStreamUserAgentRestrictionsDefaultPolicy]](../models/updatelivestreamuseragentrestrictionsdefaultpolicy.md) | :heavy_minus_sign: | The default behavior when a user-agent is not listed in `allow` or `deny`. | allow |
+| `allow` | List[*str*] | :heavy_minus_sign: | List of user-agent substrings explicitly allowed. | [
"Mozilla/55.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
] |
+| `deny` | List[*str*] | :heavy_minus_sign: | List of user-agent substrings explicitly denied. | [
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/53745.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36"
] |
\ No newline at end of file
diff --git a/docs/models/updatelivestreamuseragentrestrictionsresponsebody.md b/docs/models/updatelivestreamuseragentrestrictionsresponsebody.md
new file mode 100644
index 0000000..9a6f62f
--- /dev/null
+++ b/docs/models/updatelivestreamuseragentrestrictionsresponsebody.md
@@ -0,0 +1,11 @@
+# UpdateLiveStreamUserAgentRestrictionsResponseBody
+
+Successfully updated user-agent restrictions
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
+| `success` | *Optional[bool]* | :heavy_minus_sign: | Shows the request status. Returns true for success and false for failure. | true |
+| `data` | [Optional[models.UpdateLiveStreamUserAgentRestrictionsData]](../models/updatelivestreamuseragentrestrictionsdata.md) | :heavy_minus_sign: | N/A | |
\ No newline at end of file
diff --git a/docs/models/updatemedia.md b/docs/models/updatemedia.md
index 91f9c71..f098eb6 100644
--- a/docs/models/updatemedia.md
+++ b/docs/models/updatemedia.md
@@ -26,7 +26,7 @@
| `moderation` | [Optional[models.AiResponseRecord]](../models/airesponserecord.md) | :heavy_minus_sign: | Represents an AI response record containing status and data for AI-generated features like summary, chapters, named entities, or moderation. | |
| `is_audio_only` | *OptionalNullable[bool]* | :heavy_minus_sign: | Indicates whether the media contains only audio (no video track). | false |
| `subtitle_available` | *Optional[bool]* | :heavy_minus_sign: | Specifies whether subtitle tracks are available for the media. | false |
-| `duration` | *Optional[str]* | :heavy_minus_sign: | The length of the media in seconds, with a maximum allowed duration of 12 hours per individual media. | 00:00:10 |
+| `duration` | *Optional[float]* | :heavy_minus_sign: | Duration of the media in seconds. | 145.82 |
| `aspect_ratio` | *Optional[str]* | :heavy_minus_sign: | The aspect ratio of a video is a value that describes the relative shape of a video based on its width and height. | 16:9 |
| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was created, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
| `updated_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | Time the media was updated, defined as a localDateTime (UTC Time). | 2023-10-20T10:50:34.594302Z |
\ No newline at end of file
diff --git a/docs/sdks/liveplayback/README.md b/docs/sdks/liveplayback/README.md
index a3a3cc6..809a6fd 100644
--- a/docs/sdks/liveplayback/README.md
+++ b/docs/sdks/liveplayback/README.md
@@ -7,6 +7,8 @@
* [create_playback_id_of_stream](#create_playback_id_of_stream) - Create a playbackId
* [delete_playback_id_of_stream](#delete_playback_id_of_stream) - Delete a playbackId
* [get_live_stream_playback_id](#get_live_stream_playback_id) - Get playbackId details
+* [update_live_stream_domain_restrictions](#update_live_stream_domain_restrictions) - Update domain restrictions for a playback ID
+* [update_live_stream_user_agent_restrictions](#update_live_stream_user_agent_restrictions) - Update user-agent restrictions for a playback ID
## create_playback_id_of_stream
@@ -34,7 +36,17 @@ with Fastpix(
),
) as fastpix:
- res = fastpix.live_playback.create_playback_id_of_stream(stream_id="your-stream-id", access_policy="public")
+ res = fastpix.live_playback.create_playback_id_of_stream(stream_id="your-stream-id", access_policy="public", access_restrictions={
+ "domains": {
+ "default_policy": "deny",
+ "allow": [
+ "example.com",
+ ],
+ },
+ "user_agents": {
+ "default_policy": "allow",
+ },
+ })
# Handle response
print(json.dumps(res.model_dump(mode="json", by_alias=True, exclude_unset=True), indent=2))
@@ -47,6 +59,7 @@ with Fastpix(
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `stream_id` | *str* | :heavy_check_mark: | After creating a new live stream, FastPix assigns a unique identifier to the stream. | your-stream-id |
| `access_policy` | [Optional[models.BasicAccessPolicy]](../../models/basicaccesspolicy.md) | :heavy_minus_sign: | Basic access policy for media content | |
+| `access_restrictions` | [Optional[models.PlaybackIDAccessRestrictions]](../../models/playbackidaccessrestrictions.md) | :heavy_minus_sign: | Optional domain and user-agent access restrictions applied to the playback ID. | |
| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | |
### Response
@@ -156,4 +169,123 @@ with Fastpix(
| Error Type | Status Code | Content Type |
| -------------------------- | -------------------------- | -------------------------- |
-| errors.FastpixDefaultError | 4XX, 5XX | \*/\* |
\ No newline at end of file
+| errors.FastpixDefaultError | 4XX, 5XX | \*/\* |
+## update_live_stream_domain_restrictions
+
+This endpoint updates domain-level restrictions for a specific playback ID associated with a live stream.
+It allows you to restrict playback to specific domains or block known unauthorized domains.
+
+**How it works:**
+1. Make a `PATCH` request to this endpoint with your desired domain access configuration.
+2. Set a default policy (`allow` or `deny`) and specify domain names in the `allow` or `deny` lists.
+3. This is commonly used to restrict video playback to your website or approved client domains.
+
+**Example:**
+A streaming service can allow playback only from `example.com` and deny all others by setting: `"defaultPolicy": "deny"` and `"allow": ["example.com"]`.
+
+### Example Usage
+
+
+```python
+import os
+import json
+
+from fastpix_python import Fastpix, models
+
+with Fastpix(
+ security=models.Security(
+ username="your-access-token",
+ password="your-secret-key",
+ ),
+) as fastpix:
+
+ res = fastpix.live_playback.update_live_stream_domain_restrictions(stream_id="your-stream-id", playback_id="your-playback-id", default_policy="deny", allow=[
+ "example.com",
+ ], deny=[])
+
+ # Handle response
+ print(json.dumps(res.model_dump(mode="json", by_alias=True, exclude_unset=True), indent=2))
+
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description | Example |
+| ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| `stream_id` | *str* | :heavy_check_mark: | N/A | your-stream-id |
+| `playback_id` | *str* | :heavy_check_mark: | N/A | your-playback-id |
+| `default_policy` | [Optional[models.UpdateLiveStreamDomainRestrictionsDefaultPolicy]](../../models/updatelivestreamdomainrestrictionsdefaultpolicy.md) | :heavy_minus_sign: | Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists. | deny |
+| `allow` | List[*str*] | :heavy_minus_sign: | List of domains explicitly allowed to play the stream. | [
"example.com"
] |
+| `deny` | List[*str*] | :heavy_minus_sign: | List of domains explicitly denied from accessing the stream. | [] |
+| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | |
+
+### Response
+
+**[models.UpdateLiveStreamDomainRestrictionsResponseBody](../../models/updatelivestreamdomainrestrictionsresponsebody.md)**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| -------------------------- | -------------------------- | -------------------------- |
+| errors.FastpixDefaultError | 4XX, 5XX | \*/\* |
+
+## update_live_stream_user_agent_restrictions
+
+This endpoint allows updating user-agent restrictions for a specific playback ID associated with a live stream.
+It can be used to allow or deny specific user-agents during playback request evaluation.
+
+**How it works:**
+1. Make a `PATCH` request to this endpoint with your desired user-agent access configuration.
+2. Specify a default policy (`allow` or `deny`) and provide specific `allow` or `deny` lists.
+3. Use this to restrict access to specific browsers, devices, or bots.
+
+**Example:**
+A developer may configure a playback ID to deny access from known scraping user-agents while allowing all others by default.
+
+### Example Usage
+
+
+```python
+import os
+import json
+
+from fastpix_python import Fastpix, models
+
+with Fastpix(
+ security=models.Security(
+ username="your-access-token",
+ password="your-secret-key",
+ ),
+) as fastpix:
+
+ res = fastpix.live_playback.update_live_stream_user_agent_restrictions(stream_id="your-stream-id", playback_id="your-playback-id", default_policy="allow", allow=[
+ "Mozilla/55.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
+ ], deny=[
+ "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/53745.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36",
+ ])
+
+ # Handle response
+ print(json.dumps(res.model_dump(mode="json", by_alias=True, exclude_unset=True), indent=2))
+
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description | Example |
+| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| `stream_id` | *str* | :heavy_check_mark: | N/A | your-stream-id |
+| `playback_id` | *str* | :heavy_check_mark: | N/A | your-playback-id |
+| `default_policy` | [Optional[models.UpdateLiveStreamUserAgentRestrictionsDefaultPolicy]](../../models/updatelivestreamuseragentrestrictionsdefaultpolicy.md) | :heavy_minus_sign: | The default behavior when a user-agent is not listed in `allow` or `deny`. | allow |
+| `allow` | List[*str*] | :heavy_minus_sign: | List of user-agent substrings explicitly allowed. | [
"Mozilla/55.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
] |
+| `deny` | List[*str*] | :heavy_minus_sign: | List of user-agent substrings explicitly denied. | [
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/53745.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36"
] |
+| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | |
+
+### Response
+
+**[models.UpdateLiveStreamUserAgentRestrictionsResponseBody](../../models/updatelivestreamuseragentrestrictionsresponsebody.md)**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| -------------------------- | -------------------------- | -------------------------- |
+| errors.FastpixDefaultError | 4XX, 5XX | \*/\* |
diff --git a/docs/sdks/startlivestream/README.md b/docs/sdks/startlivestream/README.md
index f916d40..0d86ce9 100644
--- a/docs/sdks/startlivestream/README.md
+++ b/docs/sdks/startlivestream/README.md
@@ -46,6 +46,7 @@ with Fastpix(
"metadata": {
"livestream_name": "your-livestream-name",
},
+ "enable_recording": True,
})
# Handle response
diff --git a/examples/direct_upload.py b/examples/direct_upload.py
index dea79ce..484b524 100644
--- a/examples/direct_upload.py
+++ b/examples/direct_upload.py
@@ -19,6 +19,7 @@ def main():
password=os.getenv("FASTPIX_PASSWORD"),
),
)
+
with fastpix:
# 1. Create a direct upload. `cors_origin` "*" allows a browser upload from any origin.
upload = fastpix.input_video.direct_upload_video_media(request={
diff --git a/examples/live_streaming.py b/examples/live_streaming.py
index 61d2266..09f7771 100644
--- a/examples/live_streaming.py
+++ b/examples/live_streaming.py
@@ -23,14 +23,21 @@ def main():
# 1. Create a live stream.
stream = fastpix.start_live_stream.create_new_stream(
playback_settings={},
- input_media_settings={"metadata": {"livestream_name": "fastpix_livestream"}},
+ input_media_settings={"metadata": {"livestream_name": "fastpix_livestream"}, "enable_recording": True},
)
_print("create stream", stream)
stream_id = stream.data.stream_id
# 2. Give it a playback id.
- _print("create playback id", fastpix.live_playback.create_playback_id_of_stream(
+ playback = fastpix.live_playback.create_playback_id_of_stream(
stream_id=stream_id, access_policy="public",
+ )
+ _print("create playback id", playback)
+ playback_id = playback.data.id
+
+ # Restrict playback to example.com only.
+ _print("update domain restrictions", fastpix.live_playback.update_live_stream_domain_restrictions(
+ stream_id=stream_id, playback_id=playback_id, default_policy="deny", allow=["example.com"], deny=[],
))
# 3. Read + update + toggle state.
diff --git a/fastpix_python/_version.py b/fastpix_python/_version.py
index f4a4147..c0360ca 100644
--- a/fastpix_python/_version.py
+++ b/fastpix_python/_version.py
@@ -3,10 +3,10 @@
import importlib.metadata
__title__: str = "fastpix_python"
-__version__: str = "1.1.5"
+__version__: str = "1.2.0"
__openapi_doc_version__: str = "1.0.0"
__gen_version__: str = "2.723.4"
-__user_agent__: str = "fastpix-sdk/python 1.1.5 2.723.4 1.1.5 fastpix_python"
+__user_agent__: str = "fastpix-sdk/python 1.2.0 2.723.4 1.2.0 fastpix_python"
try:
if __package__ is not None:
diff --git a/fastpix_python/dimensions.py b/fastpix_python/dimensions.py
index d5db93b..153dc80 100644
--- a/fastpix_python/dimensions.py
+++ b/fastpix_python/dimensions.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import Any, List, Mapping, Optional
+from typing import Any, Mapping, Optional
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -21,7 +21,7 @@ def list_dimensions(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[str]:
+ ) -> models.ListDimensionsResponse:
r"""List the dimensions
Retrieves a list of dimensions that can be used as query parameters across various data endpoints. Each dimension has a unique id that can be used to filter data effectively.
@@ -126,7 +126,7 @@ async def list_dimensions_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[str]:
+ ) -> models.ListDimensionsResponse:
r"""List the dimensions
Retrieves a list of dimensions that can be used as query parameters across various data endpoints. Each dimension has a unique id that can be used to filter data effectively.
@@ -234,7 +234,7 @@ def list_filter_values_for_dimension(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.BrowserNameDimensiondetails]:
+ ) -> models.ListFilterValuesForDimensionResponse:
r"""List the filter values for a dimension
This endpoint returns the filter values associated with a specific dimension, along with the total number of video views for each value. For example, it can list all `browser_name` (dimension) and show how many views occurred for all available browsers like Chrome, Safari (filter values).
@@ -360,7 +360,7 @@ async def list_filter_values_for_dimension_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.BrowserNameDimensiondetails]:
+ ) -> models.ListFilterValuesForDimensionResponse:
r"""List the filter values for a dimension
This endpoint returns the filter values associated with a specific dimension, along with the total number of video views for each value. For example, it can list all `browser_name` (dimension) and show how many views occurred for all available browsers like Chrome, Safari (filter values).
diff --git a/fastpix_python/drm_configurations.py b/fastpix_python/drm_configurations.py
index ab1b44f..ea76feb 100644
--- a/fastpix_python/drm_configurations.py
+++ b/fastpix_python/drm_configurations.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import List, Mapping, NoReturn, Optional
+from typing import Mapping, NoReturn, Optional
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -47,7 +47,7 @@ def get_drm_configuration(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.DrmIDResponse]:
+ ) -> models.GetDrmConfigurationResponse:
r"""Get list of DRM configuration IDs
@@ -150,7 +150,7 @@ async def get_drm_configuration_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.DrmIDResponse]:
+ ) -> models.GetDrmConfigurationResponse:
r"""Get list of DRM configuration IDs
@@ -252,7 +252,7 @@ def get_drm_configuration_by_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.DrmIDResponse:
+ ) -> models.GetDrmConfigurationByIDResponse:
r"""Get DRM configuration by ID
@@ -351,7 +351,7 @@ async def get_drm_configuration_by_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.DrmIDResponse:
+ ) -> models.GetDrmConfigurationByIDResponse:
r"""Get DRM configuration by ID
diff --git a/fastpix_python/errors_sdk.py b/fastpix_python/errors_sdk.py
index 9445f93..d684173 100644
--- a/fastpix_python/errors_sdk.py
+++ b/fastpix_python/errors_sdk.py
@@ -24,7 +24,7 @@ def list_errors(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ListErrorsData:
+ ) -> models.ListErrorsResponse:
r"""List errors
This endpoint returns the total number of playback errors that occurred, along with the total number of views captured, based on the specified timespan and filters. It provides insights into the overall playback quality and helps identify potential issues that may impact viewer experience.
@@ -154,7 +154,7 @@ async def list_errors_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ListErrorsData:
+ ) -> models.ListErrorsResponse:
r"""List errors
This endpoint returns the total number of playback errors that occurred, along with the total number of views captured, based on the specified timespan and filters. It provides insights into the overall playback quality and helps identify potential issues that may impact viewer experience.
diff --git a/fastpix_python/in_video_ai_features.py b/fastpix_python/in_video_ai_features.py
index 77bfef2..3026fe6 100644
--- a/fastpix_python/in_video_ai_features.py
+++ b/fastpix_python/in_video_ai_features.py
@@ -48,7 +48,7 @@ def update_media_summary(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SummaryResponse:
+ ) -> models.UpdateMediaSummaryResponse:
r"""Generate video summary
This endpoint allows you to generate the summary for an existing media.
@@ -168,7 +168,7 @@ async def update_media_summary_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SummaryResponse:
+ ) -> models.UpdateMediaSummaryResponse:
r"""Generate video summary
This endpoint allows you to generate the summary for an existing media.
@@ -268,7 +268,7 @@ async def update_media_summary_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.UpdateMediaSummaryResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -287,7 +287,7 @@ def update_media_chapters(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ChaptersResponse:
+ ) -> models.UpdateMediaChaptersResponse:
r"""Generate video chapters
This endpoint enables you to generate chapters for an existing media file.
@@ -399,7 +399,7 @@ async def update_media_chapters_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ChaptersResponse:
+ ) -> models.UpdateMediaChaptersResponse:
r"""Generate video chapters
This endpoint enables you to generate chapters for an existing media file.
@@ -492,7 +492,7 @@ async def update_media_chapters_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.UpdateMediaChaptersResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -511,7 +511,7 @@ def update_media_named_entities(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.NamedEntitiesResponse:
+ ) -> models.UpdateMediaNamedEntitiesResponse:
r"""Generate named entities
This endpoint allows you to extract named entities from an existing media.
@@ -630,7 +630,7 @@ async def update_media_named_entities_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.NamedEntitiesResponse:
+ ) -> models.UpdateMediaNamedEntitiesResponse:
r"""Generate named entities
This endpoint allows you to extract named entities from an existing media.
@@ -730,7 +730,7 @@ async def update_media_named_entities_async(
return unmarshal_json_response(
models.UpdateMediaNamedEntitiesResponse, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -754,7 +754,7 @@ def update_media_moderation(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ModerationResponse:
+ ) -> models.UpdateMediaModerationResponse:
r"""Enable video moderation
This endpoint enables moderation features, such as NSFW and profanity filtering, to detect inappropriate content in existing media.
@@ -875,7 +875,7 @@ async def update_media_moderation_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ModerationResponse:
+ ) -> models.UpdateMediaModerationResponse:
r"""Enable video moderation
This endpoint enables moderation features, such as NSFW and profanity filtering, to detect inappropriate content in existing media.
@@ -972,7 +972,7 @@ async def update_media_moderation_async(
return unmarshal_json_response(
models.UpdateMediaModerationResponse, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
diff --git a/fastpix_python/input_video.py b/fastpix_python/input_video.py
index 7b94559..eb4786d 100644
--- a/fastpix_python/input_video.py
+++ b/fastpix_python/input_video.py
@@ -65,7 +65,7 @@ def create_media(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.CreateMediaResponse:
+ ) -> models.CreateMediaSuccessResponse:
r"""Create media from URL
This endpoint allows developers or users to create a new video or audio media in FastPix using a publicly accessible URL. FastPix will fetch the media from the provided URL, process it, and store it on the platform for use.
@@ -255,7 +255,7 @@ async def create_media_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.CreateMediaResponse:
+ ) -> models.CreateMediaSuccessResponse:
r"""Create media from URL
This endpoint allows developers or users to create a new video or audio media in FastPix using a publicly accessible URL. FastPix will fetch the media from the provided URL, process it, and store it on the platform for use.
@@ -431,7 +431,7 @@ def direct_upload_video_media(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.DirectUpload:
+ ) -> models.DirectUploadVideoMediaResponse:
r"""Upload media from device
This endpoint enables accelerated uploads of large media files directly from your local device to FastPix for processing and storage.
@@ -572,7 +572,7 @@ async def direct_upload_video_media_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.DirectUpload:
+ ) -> models.DirectUploadVideoMediaResponse:
r"""Upload media from device
This endpoint enables accelerated uploads of large media files directly from your local device to FastPix for processing and storage.
diff --git a/fastpix_python/live_playback.py b/fastpix_python/live_playback.py
index 1fa72a9..0c10252 100644
--- a/fastpix_python/live_playback.py
+++ b/fastpix_python/live_playback.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import Mapping, NoReturn, Optional
+from typing import List, Mapping, NoReturn, Optional, Union
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -44,11 +44,17 @@ def create_playback_id_of_stream(
*,
stream_id: str,
access_policy: Optional[models.BasicAccessPolicy] = None,
+ access_restrictions: Optional[
+ Union[
+ models.PlaybackIDAccessRestrictions,
+ models.PlaybackIDAccessRestrictionsTypedDict,
+ ]
+ ] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaybackIDSuccessResponseData:
+ ) -> models.PlaybackIDSuccessResponse:
r"""Create a playbackId
Generates a new playback ID for the live stream, allowing viewers to access the stream through this ID. The playback ID can be shared with viewers for direct access to the live broadcast.
@@ -61,6 +67,7 @@ def create_playback_id_of_stream(
:param stream_id: Upon creating a new live stream, FastPix assigns a unique identifier to the stream.
:param access_policy: Basic access policy for media content
+ :param access_restrictions: Optional domain and user-agent access restrictions applied to the playback ID.
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
@@ -80,6 +87,7 @@ def create_playback_id_of_stream(
stream_id=stream_id,
playback_id_request=models.PlaybackIDRequest(
access_policy=access_policy,
+ access_restrictions=access_restrictions,
),
)
@@ -150,11 +158,17 @@ async def create_playback_id_of_stream_async(
*,
stream_id: str,
access_policy: Optional[models.BasicAccessPolicy] = None,
+ access_restrictions: Optional[
+ Union[
+ models.PlaybackIDAccessRestrictions,
+ models.PlaybackIDAccessRestrictionsTypedDict,
+ ]
+ ] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaybackIDSuccessResponseData:
+ ) -> models.PlaybackIDSuccessResponse:
r"""Create a playbackId
Generates a new playback ID for the live stream, allowing viewers to access the stream through this ID. The playback ID can be shared with viewers for direct access to the live broadcast.
@@ -167,6 +181,7 @@ async def create_playback_id_of_stream_async(
:param stream_id: Upon creating a new live stream, FastPix assigns a unique identifier to the stream.
:param access_policy: Basic access policy for media content
+ :param access_restrictions: Optional domain and user-agent access restrictions applied to the playback ID.
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
@@ -186,6 +201,7 @@ async def create_playback_id_of_stream_async(
stream_id=stream_id,
playback_id_request=models.PlaybackIDRequest(
access_policy=access_policy,
+ access_restrictions=access_restrictions,
),
)
@@ -241,7 +257,7 @@ async def create_playback_id_of_stream_async(
if utils.match_response(http_res, "201", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.PlaybackIDSuccessResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -429,7 +445,7 @@ async def delete_playback_id_of_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.LiveStreamDeleteResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -448,7 +464,7 @@ def get_live_stream_playback_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaybackIDSuccessResponseData:
+ ) -> models.PlaybackIDSuccessResponse:
r"""Get playbackId details
Retrieves details about a previously created playback ID. If you provide the distinct `playbackId` that was given back to you in the previous stream or create playbackId request, FastPix will provide the relevant playback details such as the access policy.
@@ -542,7 +558,7 @@ async def get_live_stream_playback_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaybackIDSuccessResponseData:
+ ) -> models.PlaybackIDSuccessResponse:
r"""Get playbackId details
Retrieves details about a previously created playback ID. If you provide the distinct `playbackId` that was given back to you in the previous stream or create playbackId request, FastPix will provide the relevant playback details such as the access policy.
@@ -617,7 +633,7 @@ async def get_live_stream_playback_id_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.PlaybackIDSuccessResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -626,3 +642,495 @@ async def get_live_stream_playback_id_async(
("422", errors.ValidationErrorResponseData, errors.ValidationErrorResponse),
],
)
+
+ def update_live_stream_domain_restrictions(
+ self,
+ *,
+ stream_id: str,
+ playback_id: str,
+ default_policy: Optional[
+ models.UpdateLiveStreamDomainRestrictionsDefaultPolicy
+ ] = "allow",
+ allow: Optional[List[str]] = None,
+ deny: Optional[List[str]] = None,
+ retries: OptionalNullable[utils.RetryConfig] = UNSET,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> models.UpdateLiveStreamDomainRestrictionsResponseBody:
+ r"""Update domain restrictions for a playback ID
+
+ This endpoint updates domain-level restrictions for a specific playback ID associated with a live stream.
+ It allows you to restrict playback to specific domains or block known unauthorized domains.
+
+ **How it works:**
+ 1. Make a `PATCH` request to this endpoint with your desired domain access configuration.
+ 2. Set a default policy (`allow` or `deny`) and specify domain names in the `allow` or `deny` lists.
+ 3. This is commonly used to restrict video playback to your website or approved client domains.
+
+ **Example:**
+ A streaming service can allow playback only from `example.com` and deny all others by setting: `\"defaultPolicy\": \"deny\"` and `\"allow\": [\"example.com\"]`.
+
+
+ :param stream_id:
+ :param playback_id:
+ :param default_policy: Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists.
+ :param allow: List of domains explicitly allowed to play the stream.
+ :param deny: List of domains explicitly denied from accessing the stream.
+ :param retries: Override the default retry configuration for this method
+ :param server_url: Override the default server URL for this method
+ :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
+ :param http_headers: Additional headers to set or replace on requests.
+ """
+ base_url = None
+ url_variables = None
+ if timeout_ms is None:
+ timeout_ms = self.sdk_configuration.timeout_ms
+
+ if server_url is not None:
+ base_url = server_url
+ else:
+ base_url = self._get_url(base_url, url_variables)
+
+ request = models.UpdateLiveStreamDomainRestrictionsRequest(
+ stream_id=stream_id,
+ playback_id=playback_id,
+ body=models.UpdateLiveStreamDomainRestrictionsRequestBody(
+ default_policy=default_policy,
+ allow=allow,
+ deny=deny,
+ ),
+ )
+
+ req = self._build_request(BuildRequestData(
+ method="PATCH",
+ path="/live/streams/{streamId}/playback-ids/{playbackId}/domains",
+ base_url=base_url,
+ url_variables=url_variables,
+ request=request,
+ request_body_required=True,
+ request_has_path_params=True,
+ request_has_query_params=True,
+ user_agent_header="user-agent",
+ accept_header_value=CONTENT_TYPE_JSON,
+ http_headers=http_headers,
+ security=self.sdk_configuration.security,
+ get_serialized_body=lambda: utils.serialize_request_body(
+ request.body,
+ False,
+ False,
+ "json",
+ models.UpdateLiveStreamDomainRestrictionsRequestBody,
+ ),
+ timeout_ms=timeout_ms,
+ ))
+
+ if retries == UNSET and self.sdk_configuration.retry_config is not UNSET:
+ retries = self.sdk_configuration.retry_config
+
+ retry_config = None
+ if isinstance(retries, utils.RetryConfig):
+ retry_config = (retries, ["429", "500", "502", "503", "504"])
+
+ http_res = self.do_request(
+ hook_ctx=HookContext(
+ config=self.sdk_configuration,
+ base_url=base_url or "",
+ operation_id="update-live-stream-domain-restrictions",
+ oauth2_scopes=None,
+ security_source=get_security_from_env(
+ self.sdk_configuration.security, models.Security
+ ),
+ ),
+ request=req,
+ error_status_codes=["4XX", "5XX"],
+ retry_config=retry_config,
+ )
+
+ if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(
+ models.UpdateLiveStreamDomainRestrictionsResponseBody, http_res
+ )
+ if utils.match_response(http_res, "4XX", "*"):
+ http_res_text = utils.stream_to_text(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "5XX", "*"):
+ http_res_text = utils.stream_to_text(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "default", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(models.DefaultError, http_res)
+
+ raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
+
+ async def update_live_stream_domain_restrictions_async(
+ self,
+ *,
+ stream_id: str,
+ playback_id: str,
+ default_policy: Optional[
+ models.UpdateLiveStreamDomainRestrictionsDefaultPolicy
+ ] = "allow",
+ allow: Optional[List[str]] = None,
+ deny: Optional[List[str]] = None,
+ retries: OptionalNullable[utils.RetryConfig] = UNSET,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> models.UpdateLiveStreamDomainRestrictionsResponseBody:
+ r"""Update domain restrictions for a playback ID
+
+ This endpoint updates domain-level restrictions for a specific playback ID associated with a live stream.
+ It allows you to restrict playback to specific domains or block known unauthorized domains.
+
+ **How it works:**
+ 1. Make a `PATCH` request to this endpoint with your desired domain access configuration.
+ 2. Set a default policy (`allow` or `deny`) and specify domain names in the `allow` or `deny` lists.
+ 3. This is commonly used to restrict video playback to your website or approved client domains.
+
+ **Example:**
+ A streaming service can allow playback only from `example.com` and deny all others by setting: `\"defaultPolicy\": \"deny\"` and `\"allow\": [\"example.com\"]`.
+
+
+ :param stream_id:
+ :param playback_id:
+ :param default_policy: Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists.
+ :param allow: List of domains explicitly allowed to play the stream.
+ :param deny: List of domains explicitly denied from accessing the stream.
+ :param retries: Override the default retry configuration for this method
+ :param server_url: Override the default server URL for this method
+ :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
+ :param http_headers: Additional headers to set or replace on requests.
+ """
+ base_url = None
+ url_variables = None
+ if timeout_ms is None:
+ timeout_ms = self.sdk_configuration.timeout_ms
+
+ if server_url is not None:
+ base_url = server_url
+ else:
+ base_url = self._get_url(base_url, url_variables)
+
+ request = models.UpdateLiveStreamDomainRestrictionsRequest(
+ stream_id=stream_id,
+ playback_id=playback_id,
+ body=models.UpdateLiveStreamDomainRestrictionsRequestBody(
+ default_policy=default_policy,
+ allow=allow,
+ deny=deny,
+ ),
+ )
+
+ req = self._build_request_async(BuildRequestData(
+ method="PATCH",
+ path="/live/streams/{streamId}/playback-ids/{playbackId}/domains",
+ base_url=base_url,
+ url_variables=url_variables,
+ request=request,
+ request_body_required=True,
+ request_has_path_params=True,
+ request_has_query_params=True,
+ user_agent_header="user-agent",
+ accept_header_value=CONTENT_TYPE_JSON,
+ http_headers=http_headers,
+ security=self.sdk_configuration.security,
+ get_serialized_body=lambda: utils.serialize_request_body(
+ request.body,
+ False,
+ False,
+ "json",
+ models.UpdateLiveStreamDomainRestrictionsRequestBody,
+ ),
+ timeout_ms=timeout_ms,
+ ))
+
+ if retries == UNSET and self.sdk_configuration.retry_config is not UNSET:
+ retries = self.sdk_configuration.retry_config
+
+ retry_config = None
+ if isinstance(retries, utils.RetryConfig):
+ retry_config = (retries, ["429", "500", "502", "503", "504"])
+
+ http_res = await self.do_request_async(
+ hook_ctx=HookContext(
+ config=self.sdk_configuration,
+ base_url=base_url or "",
+ operation_id="update-live-stream-domain-restrictions",
+ oauth2_scopes=None,
+ security_source=get_security_from_env(
+ self.sdk_configuration.security, models.Security
+ ),
+ ),
+ request=req,
+ error_status_codes=["4XX", "5XX"],
+ retry_config=retry_config,
+ )
+
+ if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(
+ models.UpdateLiveStreamDomainRestrictionsResponseBody, http_res
+ )
+ if utils.match_response(http_res, "4XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "5XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "default", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(models.DefaultError, http_res)
+
+ raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
+
+ def update_live_stream_user_agent_restrictions(
+ self,
+ *,
+ stream_id: str,
+ playback_id: str,
+ default_policy: Optional[
+ models.UpdateLiveStreamUserAgentRestrictionsDefaultPolicy
+ ] = "allow",
+ allow: Optional[List[str]] = None,
+ deny: Optional[List[str]] = None,
+ retries: OptionalNullable[utils.RetryConfig] = UNSET,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> models.UpdateLiveStreamUserAgentRestrictionsResponseBody:
+ r"""Update user-agent restrictions for a playback ID
+
+ This endpoint allows updating user-agent restrictions for a specific playback ID associated with a live stream.
+ It can be used to allow or deny specific user-agents during playback request evaluation.
+
+ **How it works:**
+ 1. Make a `PATCH` request to this endpoint with your desired user-agent access configuration.
+ 2. Specify a default policy (`allow` or `deny`) and provide specific `allow` or `deny` lists.
+ 3. Use this to restrict access to specific browsers, devices, or bots.
+
+ **Example:**
+ A developer may configure a playback ID to deny access from known scraping user-agents while allowing all others by default.
+
+
+ :param stream_id:
+ :param playback_id:
+ :param default_policy: The default behavior when a user-agent is not listed in `allow` or `deny`.
+ :param allow: List of user-agent substrings explicitly allowed.
+ :param deny: List of user-agent substrings explicitly denied.
+ :param retries: Override the default retry configuration for this method
+ :param server_url: Override the default server URL for this method
+ :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
+ :param http_headers: Additional headers to set or replace on requests.
+ """
+ base_url = None
+ url_variables = None
+ if timeout_ms is None:
+ timeout_ms = self.sdk_configuration.timeout_ms
+
+ if server_url is not None:
+ base_url = server_url
+ else:
+ base_url = self._get_url(base_url, url_variables)
+
+ request = models.UpdateLiveStreamUserAgentRestrictionsRequest(
+ stream_id=stream_id,
+ playback_id=playback_id,
+ body=models.UpdateLiveStreamUserAgentRestrictionsRequestBody(
+ default_policy=default_policy,
+ allow=allow,
+ deny=deny,
+ ),
+ )
+
+ req = self._build_request(BuildRequestData(
+ method="PATCH",
+ path="/live/streams/{streamId}/playback-ids/{playbackId}/user-agents",
+ base_url=base_url,
+ url_variables=url_variables,
+ request=request,
+ request_body_required=True,
+ request_has_path_params=True,
+ request_has_query_params=True,
+ user_agent_header="user-agent",
+ accept_header_value=CONTENT_TYPE_JSON,
+ http_headers=http_headers,
+ security=self.sdk_configuration.security,
+ get_serialized_body=lambda: utils.serialize_request_body(
+ request.body,
+ False,
+ False,
+ "json",
+ models.UpdateLiveStreamUserAgentRestrictionsRequestBody,
+ ),
+ timeout_ms=timeout_ms,
+ ))
+
+ if retries == UNSET and self.sdk_configuration.retry_config is not UNSET:
+ retries = self.sdk_configuration.retry_config
+
+ retry_config = None
+ if isinstance(retries, utils.RetryConfig):
+ retry_config = (retries, ["429", "500", "502", "503", "504"])
+
+ http_res = self.do_request(
+ hook_ctx=HookContext(
+ config=self.sdk_configuration,
+ base_url=base_url or "",
+ operation_id="update-live-stream-user-agent-restrictions",
+ oauth2_scopes=None,
+ security_source=get_security_from_env(
+ self.sdk_configuration.security, models.Security
+ ),
+ ),
+ request=req,
+ error_status_codes=["4XX", "5XX"],
+ retry_config=retry_config,
+ )
+
+ if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(
+ models.UpdateLiveStreamUserAgentRestrictionsResponseBody, http_res
+ )
+ if utils.match_response(http_res, "4XX", "*"):
+ http_res_text = utils.stream_to_text(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "5XX", "*"):
+ http_res_text = utils.stream_to_text(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "default", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(models.DefaultError, http_res)
+
+ raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
+
+ async def update_live_stream_user_agent_restrictions_async(
+ self,
+ *,
+ stream_id: str,
+ playback_id: str,
+ default_policy: Optional[
+ models.UpdateLiveStreamUserAgentRestrictionsDefaultPolicy
+ ] = "allow",
+ allow: Optional[List[str]] = None,
+ deny: Optional[List[str]] = None,
+ retries: OptionalNullable[utils.RetryConfig] = UNSET,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> models.UpdateLiveStreamUserAgentRestrictionsResponseBody:
+ r"""Update user-agent restrictions for a playback ID
+
+ This endpoint allows updating user-agent restrictions for a specific playback ID associated with a live stream.
+ It can be used to allow or deny specific user-agents during playback request evaluation.
+
+ **How it works:**
+ 1. Make a `PATCH` request to this endpoint with your desired user-agent access configuration.
+ 2. Specify a default policy (`allow` or `deny`) and provide specific `allow` or `deny` lists.
+ 3. Use this to restrict access to specific browsers, devices, or bots.
+
+ **Example:**
+ A developer may configure a playback ID to deny access from known scraping user-agents while allowing all others by default.
+
+
+ :param stream_id:
+ :param playback_id:
+ :param default_policy: The default behavior when a user-agent is not listed in `allow` or `deny`.
+ :param allow: List of user-agent substrings explicitly allowed.
+ :param deny: List of user-agent substrings explicitly denied.
+ :param retries: Override the default retry configuration for this method
+ :param server_url: Override the default server URL for this method
+ :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
+ :param http_headers: Additional headers to set or replace on requests.
+ """
+ base_url = None
+ url_variables = None
+ if timeout_ms is None:
+ timeout_ms = self.sdk_configuration.timeout_ms
+
+ if server_url is not None:
+ base_url = server_url
+ else:
+ base_url = self._get_url(base_url, url_variables)
+
+ request = models.UpdateLiveStreamUserAgentRestrictionsRequest(
+ stream_id=stream_id,
+ playback_id=playback_id,
+ body=models.UpdateLiveStreamUserAgentRestrictionsRequestBody(
+ default_policy=default_policy,
+ allow=allow,
+ deny=deny,
+ ),
+ )
+
+ req = self._build_request_async(BuildRequestData(
+ method="PATCH",
+ path="/live/streams/{streamId}/playback-ids/{playbackId}/user-agents",
+ base_url=base_url,
+ url_variables=url_variables,
+ request=request,
+ request_body_required=True,
+ request_has_path_params=True,
+ request_has_query_params=True,
+ user_agent_header="user-agent",
+ accept_header_value=CONTENT_TYPE_JSON,
+ http_headers=http_headers,
+ security=self.sdk_configuration.security,
+ get_serialized_body=lambda: utils.serialize_request_body(
+ request.body,
+ False,
+ False,
+ "json",
+ models.UpdateLiveStreamUserAgentRestrictionsRequestBody,
+ ),
+ timeout_ms=timeout_ms,
+ ))
+
+ if retries == UNSET and self.sdk_configuration.retry_config is not UNSET:
+ retries = self.sdk_configuration.retry_config
+
+ retry_config = None
+ if isinstance(retries, utils.RetryConfig):
+ retry_config = (retries, ["429", "500", "502", "503", "504"])
+
+ http_res = await self.do_request_async(
+ hook_ctx=HookContext(
+ config=self.sdk_configuration,
+ base_url=base_url or "",
+ operation_id="update-live-stream-user-agent-restrictions",
+ oauth2_scopes=None,
+ security_source=get_security_from_env(
+ self.sdk_configuration.security, models.Security
+ ),
+ ),
+ request=req,
+ error_status_codes=["4XX", "5XX"],
+ retry_config=retry_config,
+ )
+
+ if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(
+ models.UpdateLiveStreamUserAgentRestrictionsResponseBody, http_res
+ )
+ if utils.match_response(http_res, "4XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "5XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "default", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(models.DefaultError, http_res)
+
+ raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
diff --git a/fastpix_python/manage_live_stream.py b/fastpix_python/manage_live_stream.py
index ea363da..b5af9c3 100644
--- a/fastpix_python/manage_live_stream.py
+++ b/fastpix_python/manage_live_stream.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import Dict, List, Mapping, NoReturn, Optional
+from typing import Dict, Mapping, NoReturn, Optional
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -50,7 +50,7 @@ def get_all_streams(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.GetCreateLiveStreamResponseDTO]:
+ ) -> models.GetStreamsResponse:
r"""Get all live streams
Retrieves a list of all live streams associated with the current workspace. It provides an overview of both current and past live streams, including details like `streamId`, `metadata`, `status`, `createdAt` and more.
@@ -148,7 +148,7 @@ async def get_all_streams_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.GetCreateLiveStreamResponseDTO]:
+ ) -> models.GetStreamsResponse:
r"""Get all live streams
Retrieves a list of all live streams associated with the current workspace. It provides an overview of both current and past live streams, including details like `streamId`, `metadata`, `status`, `createdAt` and more.
@@ -227,7 +227,7 @@ async def get_all_streams_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.GetStreamsResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -244,7 +244,7 @@ def get_live_stream_viewer_count_by_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ViewsCountResponseData:
+ ) -> models.ViewsCountResponse:
r"""Get stream views by ID
This endpoint retrieves the current number of viewers watching a specific live stream, identified by its unique `streamId`.
@@ -340,7 +340,7 @@ async def get_live_stream_viewer_count_by_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ViewsCountResponseData:
+ ) -> models.ViewsCountResponse:
r"""Get stream views by ID
This endpoint retrieves the current number of viewers watching a specific live stream, identified by its unique `streamId`.
@@ -418,7 +418,7 @@ async def get_live_stream_viewer_count_by_id_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.ViewsCountResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -436,7 +436,7 @@ def get_live_stream_by_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetCreateLiveStreamResponseDTO:
+ ) -> models.LivestreamgetResponse:
r"""Get stream by ID
This endpoint retrieves details about a specific live stream by its unique `streamId`. It includes data such as the stream’s `status` (idle, preparing, active, disabled), `metadata` (title, description), and more.
@@ -530,7 +530,7 @@ async def get_live_stream_by_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetCreateLiveStreamResponseDTO:
+ ) -> models.LivestreamgetResponse:
r"""Get stream by ID
This endpoint retrieves details about a specific live stream by its unique `streamId`. It includes data such as the stream’s `status` (idle, preparing, active, disabled), `metadata` (title, description), and more.
@@ -606,7 +606,7 @@ async def get_live_stream_by_id_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.LivestreamgetResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -800,7 +800,7 @@ async def delete_live_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.LiveStreamDeleteResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -820,7 +820,7 @@ def update_live_stream(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PatchResponseData:
+ ) -> models.PatchResponseDTO:
r"""Update a stream
This endpoint allows you to modify the parameters of an existing live stream, such as its `metadata` (title, description) or the `reconnectWindow`. It’s useful for making changes to a stream that has already been created but not yet ended. Once the live stream is disabled, you cannot update a stream.
@@ -934,7 +934,7 @@ async def update_live_stream_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PatchResponseData:
+ ) -> models.PatchResponseDTO:
r"""Update a stream
This endpoint allows you to modify the parameters of an existing live stream, such as its `metadata` (title, description) or the `reconnectWindow`. It’s useful for making changes to a stream that has already been created but not yet ended. Once the live stream is disabled, you cannot update a stream.
@@ -1028,7 +1028,7 @@ async def update_live_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.PatchResponseDTO, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -1225,7 +1225,7 @@ async def enable_live_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.LiveStreamDeleteResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.BadRequestUnion, errors.BadRequest),
@@ -1419,7 +1419,7 @@ async def disable_live_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.LiveStreamDeleteResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.StreamAlreadyDisabledErrorData, errors.StreamAlreadyDisabledError),
@@ -1625,7 +1625,7 @@ async def complete_live_stream_async(
errors.UnauthorizedErrorData, http_res
)
raise errors.UnauthorizedError(response_data, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("403", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
diff --git a/fastpix_python/manage_videos.py b/fastpix_python/manage_videos.py
index bcb7205..cd6e1fe 100644
--- a/fastpix_python/manage_videos.py
+++ b/fastpix_python/manage_videos.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import Dict, List, Mapping, NoReturn, Optional, Union
+from typing import Dict, Mapping, NoReturn, Optional, Union
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -50,7 +50,7 @@ def list_media(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.Media]:
+ ) -> models.ListMediaResponse:
r"""Get list of all media
This endpoint returns a list of all media files uploaded to FastPix within a specific workspace. Each media entry contains data such as the media `id`, `createdAt`, `status`, `type` and more. It allows you to retrieve an overview of your media assets, making it easier to manage and review them.
@@ -154,7 +154,7 @@ async def list_media_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.Media]:
+ ) -> models.ListMediaResponse:
r"""Get list of all media
This endpoint returns a list of all media files uploaded to FastPix within a specific workspace. Each media entry contains data such as the media `id`, `createdAt`, `status`, `type` and more. It allows you to retrieve an overview of your media assets, making it easier to manage and review them.
@@ -239,7 +239,7 @@ async def list_media_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.ListMediaResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -259,7 +259,7 @@ def list_live_clips(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.Media]:
+ ) -> models.ListLiveClipsResponse:
r"""Get all clips of a live stream
Retrieves a list of all media clips generated from a specific livestream. Each media entry includes metadata such as the clip media IDs, and other relevant details. A media clip is a segmented portion of an original media file (source live stream). Clips are often created for various purposes such as previews, highlights, or customized edits.
@@ -360,7 +360,7 @@ async def list_live_clips_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.Media]:
+ ) -> models.ListLiveClipsResponse:
r"""Get all clips of a live stream
Retrieves a list of all media clips generated from a specific livestream. Each media entry includes metadata such as the clip media IDs, and other relevant details. A media clip is a segmented portion of an original media file (source live stream). Clips are often created for various purposes such as previews, highlights, or customized edits.
@@ -441,7 +441,7 @@ async def list_live_clips_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.ListLiveClipsResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -458,7 +458,7 @@ def get_media(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.GetMediaResponse:
r"""Get a media by ID
By calling this endpoint, you can retrieve detailed information about a specific media item, including its current `status` and a `playbackId`. This is particularly useful for retrieving specific media details when managing large content libraries.
@@ -567,7 +567,7 @@ async def get_media_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.GetMediaResponse:
r"""Get a media by ID
By calling this endpoint, you can retrieve detailed information about a specific media item, including its current `status` and a `playbackId`. This is particularly useful for retrieving specific media details when managing large content libraries.
@@ -658,7 +658,7 @@ async def get_media_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.GetMediaResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -677,7 +677,7 @@ def updated_media(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.UpdatedMediaResponse:
r"""Update a media by ID
This endpoint allows you to update specific parameters of an existing media file. You can modify the key-value pairs of the metadata that were provided in the payload during the creation of media from a URL or when uploading the media directly from device.
@@ -792,7 +792,7 @@ async def updated_media_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.UpdatedMediaResponse:
r"""Update a media by ID
This endpoint allows you to update specific parameters of an existing media file. You can modify the key-value pairs of the metadata that were provided in the payload during the creation of media from a URL or when uploading the media directly from device.
@@ -888,7 +888,7 @@ async def updated_media_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.UpdatedMediaResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -1092,7 +1092,7 @@ async def delete_media_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.DeleteMediaResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -1113,7 +1113,7 @@ def add_media_track(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.AddTrackResponse:
+ ) -> models.AddMediaTrackResponse:
r"""Add audio / subtitle track
This endpoint allows you to add an audio or subtitle track to an existing media file using its `mediaId`. You need to provide the track `url` along with its `type` (audio or subtitle), `languageName` and `languageCode` in the request payload. You can optionally provide a `title` for the track.
@@ -1243,7 +1243,7 @@ async def add_media_track_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.AddTrackResponse:
+ ) -> models.AddMediaTrackResponse:
r"""Add audio / subtitle track
This endpoint allows you to add an audio or subtitle track to an existing media file using its `mediaId`. You need to provide the track `url` along with its `type` (audio or subtitle), `languageName` and `languageCode` in the request payload. You can optionally provide a `title` for the track.
@@ -1351,7 +1351,7 @@ async def add_media_track_async(
if utils.match_response(http_res, "201", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.AddMediaTrackResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.TrackDuplicateRequestErrorData, errors.TrackDuplicateRequestError),
@@ -1370,7 +1370,7 @@ def cancel_upload(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.MediaCancelResponse:
+ ) -> models.CancelUploadResponse:
r"""Cancel ongoing upload
This endpoint allows you to cancel ongoing upload by its `uploadId`. Once cancelled, the upload will be marked as cancelled. Use this if a user aborts an upload or if you want to programmatically stop an in-progress upload.
@@ -1473,7 +1473,7 @@ async def cancel_upload_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.MediaCancelResponse:
+ ) -> models.CancelUploadResponse:
r"""Cancel ongoing upload
This endpoint allows you to cancel ongoing upload by its `uploadId`. Once cancelled, the upload will be marked as cancelled. Use this if a user aborts an upload or if you want to programmatically stop an in-progress upload.
@@ -1557,7 +1557,7 @@ async def cancel_upload_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.CancelUploadResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.BadRequestErrorData, errors.BadRequestError),
@@ -1580,7 +1580,7 @@ def update_media_track(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.UpdateTrackResponse:
+ ) -> models.UpdateMediaTrackResponse:
r"""Update audio / subtitle track
This endpoint allows you to update an existing audio or subtitle track associated with a media file. When updating a track, you must provide the new `languageName` and `languageCode`, ensuring both parameters are included in the request. You can optionally provide a `title` for the track.
@@ -1719,7 +1719,7 @@ async def update_media_track_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.UpdateTrackResponse:
+ ) -> models.UpdateMediaTrackResponse:
r"""Update audio / subtitle track
This endpoint allows you to update an existing audio or subtitle track associated with a media file. When updating a track, you must provide the new `languageName` and `languageCode`, ensuring both parameters are included in the request. You can optionally provide a `title` for the track.
@@ -1835,7 +1835,7 @@ async def update_media_track_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.UpdateMediaTrackResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.TrackDuplicateRequestErrorData, errors.TrackDuplicateRequestError),
@@ -2068,7 +2068,7 @@ async def delete_media_track_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.DeleteMediaTrackResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -2091,7 +2091,7 @@ def generate_subtitle_track(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GenerateTrackResponse:
+ ) -> models.GenerateSubtitleTrackResponse:
r"""Generate track subtitle
This endpoint allows you to generate subtitles for an existing audio track in a media file. By calling this API, you can generate subtitles automatically using speech recognition
@@ -2223,7 +2223,7 @@ async def generate_subtitle_track_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GenerateTrackResponse:
+ ) -> models.GenerateSubtitleTrackResponse:
r"""Generate track subtitle
This endpoint allows you to generate subtitles for an existing audio track in a media file. By calling this API, you can generate subtitles automatically using speech recognition
@@ -2331,7 +2331,7 @@ async def generate_subtitle_track_async(
return unmarshal_json_response(
models.GenerateSubtitleTrackResponse, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.TrackDuplicateRequestErrorData, errors.TrackDuplicateRequestError),
@@ -2351,7 +2351,7 @@ def updated_source_access(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.UpdatedSourceAccessResponse:
r"""Update the source access of a media by ID
This endpoint allows you to update the `sourceAccess` setting of an existing media file. The `sourceAccess` parameter determines whether the original media file is accessible or restricted. Setting this to `true` enables access to the media source, while setting it to `false` restricts access.
@@ -2461,7 +2461,7 @@ async def updated_source_access_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.UpdatedSourceAccessResponse:
r"""Update the source access of a media by ID
This endpoint allows you to update the `sourceAccess` setting of an existing media file. The `sourceAccess` parameter determines whether the original media file is accessible or restricted. Setting this to `true` enables access to the media source, while setting it to `false` restricts access.
@@ -2552,7 +2552,7 @@ async def updated_source_access_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.UpdatedSourceAccessResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -2571,7 +2571,7 @@ def updated_mp4_support(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.UpdatedMp4SupportResponse:
r"""Update the mp4Support of a media by ID
This endpoint allows you to update the `mp4Support` setting of an existing media file using its media ID. You can specify the MP4 support level, such as `none`, `capped_4k`, `audioOnly`, or a combination of `audioOnly`, `capped_4k`, in the request payload.
@@ -2703,7 +2703,7 @@ async def updated_mp4_support_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Media:
+ ) -> models.UpdatedMp4SupportResponse:
r"""Update the mp4Support of a media by ID
This endpoint allows you to update the `mp4Support` setting of an existing media file using its media ID. You can specify the MP4 support level, such as `none`, `capped_4k`, `audioOnly`, or a combination of `audioOnly`, `capped_4k`, in the request payload.
@@ -2815,7 +2815,7 @@ async def updated_mp4_support_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.UpdatedMp4SupportResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.DuplicateMp4SupportErrorData, errors.DuplicateMp4SupportError),
@@ -2834,7 +2834,7 @@ def retrieve_media_input_info(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.RetrieveMediaInputInfoData:
+ ) -> models.RetrieveMediaInputInfoResponse:
r"""Get info of media inputs
Allows you to retrieve detailed information about the media inputs associated with a specific media item. You can use this endpoint to verify the media file's input URL, track creation status, and container format. The `mediaId` (either `uploadId` or `id`) must be provided to fetch the information.
@@ -2938,7 +2938,7 @@ async def retrieve_media_input_info_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.RetrieveMediaInputInfoData:
+ ) -> models.RetrieveMediaInputInfoResponse:
r"""Get info of media inputs
Allows you to retrieve detailed information about the media inputs associated with a specific media item. You can use this endpoint to verify the media file's input URL, track creation status, and container format. The `mediaId` (either `uploadId` or `id`) must be provided to fetch the information.
@@ -3024,7 +3024,7 @@ async def retrieve_media_input_info_async(
return unmarshal_json_response(
models.RetrieveMediaInputInfoResponse, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -3214,7 +3214,7 @@ async def get_summary_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.GetMediaSummaryResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -3234,7 +3234,7 @@ def list_uploads(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.DirectUpload]:
+ ) -> models.ListUploadsResponse:
r"""Get all unused upload URLs
This endpoint retrieves a paginated list of all unused upload signed URLs within your organization. It provides comprehensive metadata including upload IDs, creation dates, status, and URLs, helping you manage your media resources efficiently.
@@ -3343,7 +3343,7 @@ async def list_uploads_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.DirectUpload]:
+ ) -> models.ListUploadsResponse:
r"""Get all unused upload URLs
This endpoint retrieves a paginated list of all unused upload signed URLs within your organization. It provides comprehensive metadata including upload IDs, creation dates, status, and URLs, helping you manage your media resources efficiently.
@@ -3433,7 +3433,7 @@ async def list_uploads_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.ListUploadsResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -3453,7 +3453,7 @@ def get_media_clips(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.MediaClipResponseData]:
+ ) -> models.MediaClipResponse:
r"""Get all clips of a media
This endpoint retrieves a list of all media clips associated with a given source media ID. It helps in organizing and managing media's efficiently by providing metadata, including clip media IDs and other relevant details.
@@ -3568,7 +3568,7 @@ async def get_media_clips_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.MediaClipResponseData]:
+ ) -> models.MediaClipResponse:
r"""Get all clips of a media
This endpoint retrieves a list of all media clips associated with a given source media ID. It helps in organizing and managing media's efficiently by providing metadata, including clip media IDs and other relevant details.
@@ -3662,7 +3662,7 @@ async def get_media_clips_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.MediaClipResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
diff --git a/fastpix_python/metrics.py b/fastpix_python/metrics.py
index e3f52de..2894082 100644
--- a/fastpix_python/metrics.py
+++ b/fastpix_python/metrics.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import Any, List, Mapping, Optional
+from typing import Any, Mapping, Optional
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -30,7 +30,7 @@ def list_breakdown_values(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.MetricsBreakdownDetails]:
+ ) -> models.ListBreakdownValuesResponse:
r"""List breakdown values
Retrieves breakdown values for a specified metric and timespan, allowing you to analyze the performance of your content based on various dimensions. It provides insights into how different factors contribute to the overall metrics.
@@ -181,7 +181,7 @@ async def list_breakdown_values_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.MetricsBreakdownDetails]:
+ ) -> models.ListBreakdownValuesResponse:
r"""List breakdown values
Retrieves breakdown values for a specified metric and timespan, allowing you to analyze the performance of your content based on various dimensions. It provides insights into how different factors contribute to the overall metrics.
@@ -327,7 +327,7 @@ def list_overall_values(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.MetricsOverallDataDetails:
+ ) -> models.ListOverallValuesResponse:
r"""List overall values
Retrieves overall values for a specified metric, providing summary statistics that help you understand the performance of your content. The response includes key metrics such as `totalWatchTime`, `uniqueViews`, `totalPlayTime` and `totalViews`.
@@ -470,7 +470,7 @@ async def list_overall_values_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.MetricsOverallDataDetails:
+ ) -> models.ListOverallValuesResponse:
r"""List overall values
Retrieves overall values for a specified metric, providing summary statistics that help you understand the performance of your content. The response includes key metrics such as `totalWatchTime`, `uniqueViews`, `totalPlayTime` and `totalViews`.
@@ -615,7 +615,7 @@ def get_timeseries_data(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.MetricsTimeseriesDataDetails]:
+ ) -> models.GetTimeseriesDataResponse:
r"""Get timeseries data
This endpoint retrieves timeseries data for a specified metric, providing insights into how the metric values change over time. The response includes an array of data points, each representing the metric's value at specific intervals.
@@ -744,7 +744,7 @@ async def get_timeseries_data_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.MetricsTimeseriesDataDetails]:
+ ) -> models.GetTimeseriesDataResponse:
r"""Get timeseries data
This endpoint retrieves timeseries data for a specified metric, providing insights into how the metric values change over time. The response includes an array of data points, each representing the metric's value at specific intervals.
@@ -871,7 +871,7 @@ def list_comparison_values(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[List[models.MetricsComparisonDetails]]:
+ ) -> models.ListComparisonValuesResponse:
r"""List comparison values
This endpoint allows you to compare multiple metrics across specified dimensions. You can specify the metrics you want to compare in the query parameters, and the response will include the relevant metrics for the specified dimensions.
@@ -1001,7 +1001,7 @@ async def list_comparison_values_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[List[models.MetricsComparisonDetails]]:
+ ) -> models.ListComparisonValuesResponse:
r"""List comparison values
This endpoint allows you to compare multiple metrics across specified dimensions. You can specify the metrics you want to compare in the query parameters, and the response will include the relevant metrics for the specified dimensions.
diff --git a/fastpix_python/models/__init__.py b/fastpix_python/models/__init__.py
index 946e6fc..35b4404 100644
--- a/fastpix_python/models/__init__.py
+++ b/fastpix_python/models/__init__.py
@@ -1529,6 +1529,28 @@
"UpdateUserAgentRestrictionsDataTypedDict",
"UpdateUserAgentRestrictionsResponseBodyTypedDict",
],
+ ".update_live_stream_domain_restrictionsop": [
+ "UpdateLiveStreamDomainRestrictionsData",
+ "UpdateLiveStreamDomainRestrictionsRequest",
+ "UpdateLiveStreamDomainRestrictionsRequestBody",
+ "UpdateLiveStreamDomainRestrictionsResponseBody",
+ "UpdateLiveStreamDomainRestrictionsDefaultPolicy",
+ "UpdateLiveStreamDomainRestrictionsRequestBodyTypedDict",
+ "UpdateLiveStreamDomainRestrictionsRequestTypedDict",
+ "UpdateLiveStreamDomainRestrictionsDataTypedDict",
+ "UpdateLiveStreamDomainRestrictionsResponseBodyTypedDict",
+ ],
+ ".update_live_stream_user_agent_restrictionsop": [
+ "UpdateLiveStreamUserAgentRestrictionsData",
+ "UpdateLiveStreamUserAgentRestrictionsRequest",
+ "UpdateLiveStreamUserAgentRestrictionsRequestBody",
+ "UpdateLiveStreamUserAgentRestrictionsResponseBody",
+ "UpdateLiveStreamUserAgentRestrictionsDefaultPolicy",
+ "UpdateLiveStreamUserAgentRestrictionsRequestBodyTypedDict",
+ "UpdateLiveStreamUserAgentRestrictionsRequestTypedDict",
+ "UpdateLiveStreamUserAgentRestrictionsDataTypedDict",
+ "UpdateLiveStreamUserAgentRestrictionsResponseBodyTypedDict",
+ ],
".default_error": [
"Error",
"ErrorTypedDict",
diff --git a/fastpix_python/models/createlivestreamrequest.py b/fastpix_python/models/createlivestreamrequest.py
index 8f7201d..f9444ae 100644
--- a/fastpix_python/models/createlivestreamrequest.py
+++ b/fastpix_python/models/createlivestreamrequest.py
@@ -30,6 +30,8 @@ class InputMediaSettingsTypedDict(TypedDict):
r"""You can search for videos with specific key value pairs using metadata, when you tag a video in \"key\":\"value\"s pairs. Dynamic Metadata allows you to define a key that allows any value pair. You can have maximum of 255 characters and upto 10 entries are allowed."""
enable_dvr_mode: NotRequired[bool]
r"""Enables DVR (Digital Video Recorder) functionality for the live stream. When set to true, viewers can pause, rewind, and resume playback during the live broadcast. This allows time-shifted viewing of the stream while it is still ongoing."""
+ enable_recording: NotRequired[bool]
+ r"""Controls whether the livestream is recorded to a VOD asset (Live-to-VOD). When set to true (default), FastPix records and stores the livestream for on-demand viewing. When set to false, the livestream is not recorded."""
class InputMediaSettings(BaseModel):
@@ -59,6 +61,11 @@ class InputMediaSettings(BaseModel):
] = None
r"""Enables DVR (Digital Video Recorder) functionality for the live stream. When set to true, viewers can pause, rewind, and resume playback during the live broadcast. This allows time-shifted viewing of the stream while it is still ongoing."""
+ enable_recording: Annotated[
+ Optional[bool], pydantic.Field(alias="enableRecording")
+ ] = True
+ r"""Controls whether the livestream is recorded to a VOD asset (Live-to-VOD). When set to true (default), FastPix records and stores the livestream for on-demand viewing. When set to false, the livestream is not recorded."""
+
class CreateLiveStreamRequestTypedDict(TypedDict):
playback_settings: PlaybackSettingsTypedDict
diff --git a/fastpix_python/models/media.py b/fastpix_python/models/media.py
index 666ba90..d552195 100644
--- a/fastpix_python/models/media.py
+++ b/fastpix_python/models/media.py
@@ -124,7 +124,7 @@ class MediaTypedDict(TypedDict):
r"""A collection of Playback ID objects utilized for crafting HLS playback URLs."""
tracks: NotRequired[List[TrackTypedDict]]
r"""A media consists of different media tracks, like video, audio, and subtitle, all combined."""
- duration: NotRequired[str]
+ duration: NotRequired[float]
r"""The time span of the media, measured in seconds with a maximum allowable duration of 12 hours per individual media."""
frame_rate: NotRequired[str]
r"""Frame rate quantifies the speed at which frames are displayed per second. It represents the range of frames available for a specific track. If the frame rate of the input file is indeterminable, it will be indicated by a value of -1."""
@@ -203,7 +203,7 @@ class Media(BaseModel):
tracks: Optional[List[Track]] = None
r"""A media consists of different media tracks, like video, audio, and subtitle, all combined."""
- duration: Optional[str] = None
+ duration: Optional[float] = None
r"""The time span of the media, measured in seconds with a maximum allowable duration of 12 hours per individual media."""
frame_rate: Annotated[Optional[str], pydantic.Field(alias="frameRate")] = None
diff --git a/fastpix_python/models/mediaclipresponse.py b/fastpix_python/models/mediaclipresponse.py
index a7b4c3d..95e8f73 100644
--- a/fastpix_python/models/mediaclipresponse.py
+++ b/fastpix_python/models/mediaclipresponse.py
@@ -205,8 +205,8 @@ class MediaClipResponseDataTypedDict(TypedDict):
r"""Indicates whether subtitles are available for the media."""
optimize_audio: NotRequired[bool]
r"""Whether the audio track of the media has been volume-normalized."""
- duration: NotRequired[str]
- r"""The total duration of the media."""
+ duration: NotRequired[float]
+ r"""The total duration of the media in seconds."""
aspect_ratio: NotRequired[str]
r"""The aspect ratio of the media."""
created_at: NotRequired[datetime]
@@ -276,8 +276,8 @@ class MediaClipResponseData(BaseModel):
] = None
r"""Whether the audio track of the media has been volume-normalized."""
- duration: Optional[str] = None
- r"""The total duration of the media."""
+ duration: Optional[float] = None
+ r"""The total duration of the media in seconds."""
aspect_ratio: Annotated[Optional[str], pydantic.Field(alias="aspectRatio")] = None
r"""The aspect ratio of the media."""
diff --git a/fastpix_python/models/playbackidrequest.py b/fastpix_python/models/playbackidrequest.py
index 0b8aa53..4b16426 100644
--- a/fastpix_python/models/playbackidrequest.py
+++ b/fastpix_python/models/playbackidrequest.py
@@ -2,6 +2,10 @@
from __future__ import annotations
from .basicaccesspolicy import BasicAccessPolicy
+from .playbackidresponse import (
+ PlaybackIDAccessRestrictions,
+ PlaybackIDAccessRestrictionsTypedDict,
+)
from ..types import BaseModel
import pydantic
from typing import Optional
@@ -11,6 +15,8 @@
class PlaybackIDRequestTypedDict(TypedDict):
access_policy: NotRequired[BasicAccessPolicy]
r"""Basic access policy for media content"""
+ access_restrictions: NotRequired[PlaybackIDAccessRestrictionsTypedDict]
+ r"""Optional domain and user-agent access restrictions applied to the playback ID."""
class PlaybackIDRequest(BaseModel):
@@ -18,3 +24,9 @@ class PlaybackIDRequest(BaseModel):
Optional[BasicAccessPolicy], pydantic.Field(alias="accessPolicy")
] = None
r"""Basic access policy for media content"""
+
+ access_restrictions: Annotated[
+ Optional[PlaybackIDAccessRestrictions],
+ pydantic.Field(alias="accessRestrictions"),
+ ] = None
+ r"""Optional domain and user-agent access restrictions applied to the playback ID."""
diff --git a/fastpix_python/models/playbackidsuccessresponse.py b/fastpix_python/models/playbackidsuccessresponse.py
index 36be609..bf7c48e 100644
--- a/fastpix_python/models/playbackidsuccessresponse.py
+++ b/fastpix_python/models/playbackidsuccessresponse.py
@@ -1,6 +1,10 @@
"""Code generated by fastpix (https://fastpix.com). DO NOT EDIT."""
from __future__ import annotations
+from .playbackidresponse import (
+ PlaybackIDAccessRestrictions,
+ PlaybackIDAccessRestrictionsTypedDict,
+)
from ..types import BaseModel
import pydantic
from typing import Optional
@@ -12,6 +16,8 @@ class PlaybackIDSuccessResponseDataTypedDict(TypedDict):
r"""Unique identifier for the playbackId"""
access_policy: NotRequired[str]
r"""Determines if access to the streamed content is kept private or available to all."""
+ access_restrictions: NotRequired[PlaybackIDAccessRestrictionsTypedDict]
+ r"""Optional domain and user-agent access restrictions applied to the playback ID."""
class PlaybackIDSuccessResponseData(BaseModel):
@@ -21,6 +27,12 @@ class PlaybackIDSuccessResponseData(BaseModel):
access_policy: Annotated[Optional[str], pydantic.Field(alias="accessPolicy")] = None
r"""Determines if access to the streamed content is kept private or available to all."""
+ access_restrictions: Annotated[
+ Optional[PlaybackIDAccessRestrictions],
+ pydantic.Field(alias="accessRestrictions"),
+ ] = None
+ r"""Optional domain and user-agent access restrictions applied to the playback ID."""
+
class PlaybackIDSuccessResponseTypedDict(TypedDict):
r"""Displays the result of the request."""
diff --git a/fastpix_python/models/playbacksettings.py b/fastpix_python/models/playbacksettings.py
index 3c752a0..f803dc2 100644
--- a/fastpix_python/models/playbacksettings.py
+++ b/fastpix_python/models/playbacksettings.py
@@ -2,6 +2,10 @@
from __future__ import annotations
from .basicaccesspolicy import BasicAccessPolicy
+from .playbackidresponse import (
+ PlaybackIDAccessRestrictions,
+ PlaybackIDAccessRestrictionsTypedDict,
+)
from ..types import BaseModel
import pydantic
from typing import Optional
@@ -13,6 +17,8 @@ class PlaybackSettingsTypedDict(TypedDict):
access_policy: NotRequired[BasicAccessPolicy]
r"""Basic access policy for media content"""
+ access_restrictions: NotRequired[PlaybackIDAccessRestrictionsTypedDict]
+ r"""Optional domain and user-agent access restrictions applied to the playback ID."""
class PlaybackSettings(BaseModel):
@@ -22,3 +28,9 @@ class PlaybackSettings(BaseModel):
Optional[BasicAccessPolicy], pydantic.Field(alias="accessPolicy")
] = None
r"""Basic access policy for media content"""
+
+ access_restrictions: Annotated[
+ Optional[PlaybackIDAccessRestrictions],
+ pydantic.Field(alias="accessRestrictions"),
+ ] = None
+ r"""Optional domain and user-agent access restrictions applied to the playback ID."""
diff --git a/fastpix_python/models/playlistbyidresponse.py b/fastpix_python/models/playlistbyidresponse.py
index ed007b6..b7a4245 100644
--- a/fastpix_python/models/playlistbyidresponse.py
+++ b/fastpix_python/models/playlistbyidresponse.py
@@ -18,8 +18,8 @@
class PlaylistByIDResponseMediaListTypedDict(TypedDict):
created_at: NotRequired[datetime]
r"""Timestamp of media creation in the workspace."""
- duration: NotRequired[str]
- r"""Duration of the media in hh:mm:ss format."""
+ duration: NotRequired[float]
+ r"""Duration of the media in seconds."""
id: NotRequired[str]
r"""unique id of the particular media."""
source_resolution: NotRequired[str]
@@ -34,8 +34,8 @@ class PlaylistByIDResponseMediaList(BaseModel):
created_at: Annotated[Optional[datetime], pydantic.Field(alias="createdAt")] = None
r"""Timestamp of media creation in the workspace."""
- duration: Optional[str] = None
- r"""Duration of the media in hh:mm:ss format."""
+ duration: Optional[float] = None
+ r"""Duration of the media in seconds."""
id: Optional[str] = None
r"""unique id of the particular media."""
diff --git a/fastpix_python/models/playlistcreatedschema.py b/fastpix_python/models/playlistcreatedschema.py
index 1928020..c9eb66c 100644
--- a/fastpix_python/models/playlistcreatedschema.py
+++ b/fastpix_python/models/playlistcreatedschema.py
@@ -43,8 +43,8 @@ class PlaylistCreatedSchemaMetadata(BaseModel):
class PlaylistCreatedSchemaMediaListTypedDict(TypedDict):
created_at: NotRequired[datetime]
r"""timestamp of media creation in the workspace"""
- duration: NotRequired[str]
- r"""duration of the media in hh:mm:ss format"""
+ duration: NotRequired[float]
+ r"""Duration of the media in seconds."""
id: NotRequired[str]
r"""unique identifier of the media"""
source_resolution: NotRequired[str]
@@ -59,8 +59,8 @@ class PlaylistCreatedSchemaMediaList(BaseModel):
created_at: Annotated[Optional[datetime], pydantic.Field(alias="createdAt")] = None
r"""timestamp of media creation in the workspace"""
- duration: Optional[str] = None
- r"""duration of the media in hh:mm:ss format"""
+ duration: Optional[float] = None
+ r"""Duration of the media in seconds."""
id: Optional[str] = None
r"""unique identifier of the media"""
diff --git a/fastpix_python/models/update_live_stream_domain_restrictionsop.py b/fastpix_python/models/update_live_stream_domain_restrictionsop.py
new file mode 100644
index 0000000..50bc5db
--- /dev/null
+++ b/fastpix_python/models/update_live_stream_domain_restrictionsop.py
@@ -0,0 +1,171 @@
+"""
+This file is auto-generated.
+Do not edit this file manually.
+Your changes will be overwritten during the next generation.
+"""
+
+from __future__ import annotations
+from .default_error import DefaultError, DefaultErrorTypedDict
+from ..types import BaseModel, UNSET_SENTINEL
+from ..utils import FieldMetadata, PathParamMetadata, RequestMetadata
+import pydantic
+from pydantic import model_serializer
+from typing import List, Literal, Optional, Union
+from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
+
+
+UpdateLiveStreamDomainRestrictionsDefaultPolicy = Literal[
+ "allow",
+ "deny",
+]
+r"""Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists."""
+
+
+class UpdateLiveStreamDomainRestrictionsRequestBodyTypedDict(TypedDict):
+ default_policy: NotRequired[UpdateLiveStreamDomainRestrictionsDefaultPolicy]
+ r"""Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists."""
+ allow: NotRequired[List[str]]
+ r"""List of domains explicitly allowed to play the stream."""
+ deny: NotRequired[List[str]]
+ r"""List of domains explicitly denied from accessing the stream."""
+
+
+class UpdateLiveStreamDomainRestrictionsRequestBody(BaseModel):
+ default_policy: Annotated[
+ Optional[UpdateLiveStreamDomainRestrictionsDefaultPolicy],
+ pydantic.Field(alias="defaultPolicy"),
+ ] = "allow"
+ r"""Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists."""
+
+ allow: Optional[List[str]] = None
+ r"""List of domains explicitly allowed to play the stream."""
+
+ deny: Optional[List[str]] = None
+ r"""List of domains explicitly denied from accessing the stream."""
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = {"defaultPolicy", "allow", "deny"}
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k)
+
+ if val != UNSET_SENTINEL and (
+ val is not None or k not in optional_fields
+ ):
+ m[k] = val
+
+ return m
+
+
+class UpdateLiveStreamDomainRestrictionsRequestTypedDict(TypedDict):
+ stream_id: str
+ playback_id: str
+ body: UpdateLiveStreamDomainRestrictionsRequestBodyTypedDict
+
+
+class UpdateLiveStreamDomainRestrictionsRequest(BaseModel):
+ stream_id: Annotated[
+ str,
+ pydantic.Field(alias="streamId"),
+ FieldMetadata(path=PathParamMetadata(style="simple", explode=False)),
+ ]
+
+ playback_id: Annotated[
+ str,
+ pydantic.Field(alias="playbackId"),
+ FieldMetadata(path=PathParamMetadata(style="simple", explode=False)),
+ ]
+
+ body: Annotated[
+ UpdateLiveStreamDomainRestrictionsRequestBody,
+ FieldMetadata(request=RequestMetadata(media_type="application/json")),
+ ]
+
+
+class UpdateLiveStreamDomainRestrictionsDataTypedDict(TypedDict):
+ default_policy: NotRequired[str]
+ r"""Specify the fallback behavior for domains that are not listed in the allow or deny lists."""
+ allow: NotRequired[List[str]]
+ r"""List of domains explicitly allowed to play the stream."""
+ deny: NotRequired[List[str]]
+ r"""List of domains explicitly denied from accessing the stream."""
+
+
+class UpdateLiveStreamDomainRestrictionsData(BaseModel):
+ default_policy: Annotated[Optional[str], pydantic.Field(alias="defaultPolicy")] = (
+ None
+ )
+ r"""Specify the fallback behavior for domains that are not listed in the allow or deny lists."""
+
+ allow: Optional[List[str]] = None
+ r"""List of domains explicitly allowed to play the stream."""
+
+ deny: Optional[List[str]] = None
+ r"""List of domains explicitly denied from accessing the stream."""
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = {"defaultPolicy", "allow", "deny"}
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k)
+
+ if val != UNSET_SENTINEL and (
+ val is not None or k not in optional_fields
+ ):
+ m[k] = val
+
+ return m
+
+
+class UpdateLiveStreamDomainRestrictionsResponseBodyTypedDict(TypedDict):
+ r"""Successfully updated domain restrictions"""
+
+ success: NotRequired[bool]
+ r"""Shows the request status. Returns true for success and false for failure."""
+ data: NotRequired[UpdateLiveStreamDomainRestrictionsDataTypedDict]
+
+
+class UpdateLiveStreamDomainRestrictionsResponseBody(BaseModel):
+ r"""Successfully updated domain restrictions"""
+
+ success: Optional[bool] = None
+ r"""Shows the request status. Returns true for success and false for failure."""
+
+ data: Optional[UpdateLiveStreamDomainRestrictionsData] = None
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = {"success", "data"}
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k)
+
+ if val != UNSET_SENTINEL and (
+ val is not None or k not in optional_fields
+ ):
+ m[k] = val
+
+ return m
+
+
+UpdateLiveStreamDomainRestrictionsResponseTypedDict = TypeAliasType(
+ "UpdateLiveStreamDomainRestrictionsResponseTypedDict",
+ Union[UpdateLiveStreamDomainRestrictionsResponseBodyTypedDict, DefaultErrorTypedDict],
+)
+
+
+UpdateLiveStreamDomainRestrictionsResponse = TypeAliasType(
+ "UpdateLiveStreamDomainRestrictionsResponse",
+ Union[UpdateLiveStreamDomainRestrictionsResponseBody, DefaultError],
+)
diff --git a/fastpix_python/models/update_live_stream_user_agent_restrictionsop.py b/fastpix_python/models/update_live_stream_user_agent_restrictionsop.py
new file mode 100644
index 0000000..c66cd68
--- /dev/null
+++ b/fastpix_python/models/update_live_stream_user_agent_restrictionsop.py
@@ -0,0 +1,171 @@
+"""
+This file is auto-generated.
+Do not edit this file manually.
+Your changes will be overwritten during the next generation.
+"""
+
+from __future__ import annotations
+from .default_error import DefaultError, DefaultErrorTypedDict
+from ..types import BaseModel, UNSET_SENTINEL
+from ..utils import FieldMetadata, PathParamMetadata, RequestMetadata
+import pydantic
+from pydantic import model_serializer
+from typing import List, Literal, Optional, Union
+from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
+
+
+UpdateLiveStreamUserAgentRestrictionsDefaultPolicy = Literal[
+ "allow",
+ "deny",
+]
+r"""The default behavior when a user-agent is not listed in `allow` or `deny`."""
+
+
+class UpdateLiveStreamUserAgentRestrictionsRequestBodyTypedDict(TypedDict):
+ default_policy: NotRequired[UpdateLiveStreamUserAgentRestrictionsDefaultPolicy]
+ r"""The default behavior when a user-agent is not listed in `allow` or `deny`."""
+ allow: NotRequired[List[str]]
+ r"""List of user-agent substrings explicitly allowed."""
+ deny: NotRequired[List[str]]
+ r"""List of user-agent substrings explicitly denied."""
+
+
+class UpdateLiveStreamUserAgentRestrictionsRequestBody(BaseModel):
+ default_policy: Annotated[
+ Optional[UpdateLiveStreamUserAgentRestrictionsDefaultPolicy],
+ pydantic.Field(alias="defaultPolicy"),
+ ] = "allow"
+ r"""The default behavior when a user-agent is not listed in `allow` or `deny`."""
+
+ allow: Optional[List[str]] = None
+ r"""List of user-agent substrings explicitly allowed."""
+
+ deny: Optional[List[str]] = None
+ r"""List of user-agent substrings explicitly denied."""
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = {"defaultPolicy", "allow", "deny"}
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k)
+
+ if val != UNSET_SENTINEL and (
+ val is not None or k not in optional_fields
+ ):
+ m[k] = val
+
+ return m
+
+
+class UpdateLiveStreamUserAgentRestrictionsRequestTypedDict(TypedDict):
+ stream_id: str
+ playback_id: str
+ body: UpdateLiveStreamUserAgentRestrictionsRequestBodyTypedDict
+
+
+class UpdateLiveStreamUserAgentRestrictionsRequest(BaseModel):
+ stream_id: Annotated[
+ str,
+ pydantic.Field(alias="streamId"),
+ FieldMetadata(path=PathParamMetadata(style="simple", explode=False)),
+ ]
+
+ playback_id: Annotated[
+ str,
+ pydantic.Field(alias="playbackId"),
+ FieldMetadata(path=PathParamMetadata(style="simple", explode=False)),
+ ]
+
+ body: Annotated[
+ UpdateLiveStreamUserAgentRestrictionsRequestBody,
+ FieldMetadata(request=RequestMetadata(media_type="application/json")),
+ ]
+
+
+class UpdateLiveStreamUserAgentRestrictionsDataTypedDict(TypedDict):
+ default_policy: NotRequired[str]
+ r"""Specifies the default behavior for user agents not listed in the allow or deny lists."""
+ allow: NotRequired[List[str]]
+ r"""List of user-agent substrings explicitly allowed."""
+ deny: NotRequired[List[str]]
+ r"""List of user-agent substrings explicitly denied."""
+
+
+class UpdateLiveStreamUserAgentRestrictionsData(BaseModel):
+ default_policy: Annotated[Optional[str], pydantic.Field(alias="defaultPolicy")] = (
+ None
+ )
+ r"""Specifies the default behavior for user agents not listed in the allow or deny lists."""
+
+ allow: Optional[List[str]] = None
+ r"""List of user-agent substrings explicitly allowed."""
+
+ deny: Optional[List[str]] = None
+ r"""List of user-agent substrings explicitly denied."""
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = {"defaultPolicy", "allow", "deny"}
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k)
+
+ if val != UNSET_SENTINEL and (
+ val is not None or k not in optional_fields
+ ):
+ m[k] = val
+
+ return m
+
+
+class UpdateLiveStreamUserAgentRestrictionsResponseBodyTypedDict(TypedDict):
+ r"""Successfully updated user-agent restrictions"""
+
+ success: NotRequired[bool]
+ r"""Shows the request status. Returns true for success and false for failure."""
+ data: NotRequired[UpdateLiveStreamUserAgentRestrictionsDataTypedDict]
+
+
+class UpdateLiveStreamUserAgentRestrictionsResponseBody(BaseModel):
+ r"""Successfully updated user-agent restrictions"""
+
+ success: Optional[bool] = None
+ r"""Shows the request status. Returns true for success and false for failure."""
+
+ data: Optional[UpdateLiveStreamUserAgentRestrictionsData] = None
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = {"success", "data"}
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k)
+
+ if val != UNSET_SENTINEL and (
+ val is not None or k not in optional_fields
+ ):
+ m[k] = val
+
+ return m
+
+
+UpdateLiveStreamUserAgentRestrictionsResponseTypedDict = TypeAliasType(
+ "UpdateLiveStreamUserAgentRestrictionsResponseTypedDict",
+ Union[UpdateLiveStreamUserAgentRestrictionsResponseBodyTypedDict, DefaultErrorTypedDict],
+)
+
+
+UpdateLiveStreamUserAgentRestrictionsResponse = TypeAliasType(
+ "UpdateLiveStreamUserAgentRestrictionsResponse",
+ Union[UpdateLiveStreamUserAgentRestrictionsResponseBody, DefaultError],
+)
diff --git a/fastpix_python/playback.py b/fastpix_python/playback.py
index 4a2387b..1847a62 100644
--- a/fastpix_python/playback.py
+++ b/fastpix_python/playback.py
@@ -56,7 +56,7 @@ def create_media_playback_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.CreateMediaPlaybackIDData:
+ ) -> models.CreateMediaPlaybackIDResponse:
r"""Create a playback ID
You can create a new playback ID for a specific media asset. If you have already retrieved an existing `playbackId` using the Get Media by ID endpoint for a media asset, you can use this endpoint to generate a new playback ID with a specified access policy.
@@ -192,7 +192,7 @@ async def create_media_playback_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.CreateMediaPlaybackIDData:
+ ) -> models.CreateMediaPlaybackIDResponse:
r"""Create a playback ID
You can create a new playback ID for a specific media asset. If you have already retrieved an existing `playbackId` using the Get Media by ID endpoint for a media asset, you can use this endpoint to generate a new playback ID with a specified access policy.
@@ -301,7 +301,7 @@ async def create_media_playback_id_async(
return unmarshal_json_response(
models.CreateMediaPlaybackIDResponse, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -511,7 +511,7 @@ async def delete_media_playback_id_async(
return unmarshal_json_response(
models.DeleteMediaPlaybackIDResponse, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -530,7 +530,7 @@ def get_playback_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetPlaybackIDData:
+ ) -> models.GetPlaybackIDResponse:
r"""Get a playback ID
This endpoint retrieves details about a specific playback ID associated with a media asset. This endpoint is commonly used to check the access policy (e.g., public or private) with the specific playback ID.
@@ -629,7 +629,7 @@ async def get_playback_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetPlaybackIDData:
+ ) -> models.GetPlaybackIDResponse:
r"""Get a playback ID
This endpoint retrieves details about a specific playback ID associated with a media asset. This endpoint is commonly used to check the access policy (e.g., public or private) with the specific playback ID.
@@ -709,7 +709,7 @@ async def get_playback_id_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.GetPlaybackIDResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.InvalidPermissionErrorData, errors.InvalidPermissionError),
@@ -727,7 +727,7 @@ def list_playback_ids(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ListPlaybackIdsResponse:
+ ) -> models.ListPlaybackIdsResponseBody:
r"""Get all playback IDs details for a media
Retrieves all playback IDs associated with a given media asset, including each playback ID’s access policy and detailed access restrictions such as allowed or denied domains and user agents.
@@ -824,7 +824,7 @@ async def list_playback_ids_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.ListPlaybackIdsResponse:
+ ) -> models.ListPlaybackIdsResponseBody:
r"""Get all playback IDs details for a media
Retrieves all playback IDs associated with a given media asset, including each playback ID’s access policy and detailed access restrictions such as allowed or denied domains and user agents.
@@ -1159,3 +1159,248 @@ def update_user_agent_restrictions(
raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
+ async def update_domain_restrictions_async(
+ self,
+ *,
+ media_id: str,
+ playback_id: str,
+ default_policy: Optional[
+ models.UpdateDomainRestrictionsDefaultPolicy
+ ] = "allow",
+ allow: Optional[List[str]] = None,
+ deny: Optional[List[str]] = None,
+ retries: OptionalNullable[utils.RetryConfig] = UNSET,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> models.UpdateDomainRestrictionsResponseBody:
+ r"""Update domain restrictions for a playback ID
+
+ This endpoint updates domain-level restrictions for a specific playback ID associated with a media asset.
+ It allows you to restrict playback to specific domains or block known unauthorized domains.
+
+ **How it works:**
+ 1. Make a `PATCH` request to this endpoint with your desired domain access configuration.
+ 2. Set a default policy (`allow` or `deny`) and specify domain names in the `allow` or `deny` lists.
+ 3. This is commonly used to restrict video playback to your website or approved client domains.
+
+ **Example:**
+ A streaming service can allow playback only from `example.com` and deny all others by setting: `\"defaultPolicy\": \"deny\"` and `\"allow\": [\"example.com\"]`.
+
+
+ :param media_id:
+ :param playback_id:
+ :param default_policy: Specify the fallback behavior for domains that are not listed in the `allow` or `deny` lists.
+ :param allow: List of domains explicitly allowed to play the media.
+ :param deny: List of domains explicitly denied from accessing the media.
+ :param retries: Override the default retry configuration for this method
+ :param server_url: Override the default server URL for this method
+ :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
+ :param http_headers: Additional headers to set or replace on requests.
+ """
+ base_url = None
+ url_variables = None
+ if timeout_ms is None:
+ timeout_ms = self.sdk_configuration.timeout_ms
+
+ if server_url is not None:
+ base_url = server_url
+ else:
+ base_url = self._get_url(base_url, url_variables)
+
+ request = models.UpdateDomainRestrictionsRequest(
+ media_id=media_id,
+ playback_id=playback_id,
+ body=models.UpdateDomainRestrictionsRequestBody(
+ default_policy=default_policy,
+ allow=allow,
+ deny=deny,
+ ),
+ )
+
+ req = self._build_request_async(BuildRequestData(
+ method="PATCH",
+ path="/on-demand/{mediaId}/playback-ids/{playbackId}/domains",
+ base_url=base_url,
+ url_variables=url_variables,
+ request=request,
+ request_body_required=True,
+ request_has_path_params=True,
+ request_has_query_params=True,
+ user_agent_header="user-agent",
+ accept_header_value=CONTENT_TYPE_JSON,
+ http_headers=http_headers,
+ security=self.sdk_configuration.security,
+ get_serialized_body=lambda: utils.serialize_request_body(
+ request.body,
+ False,
+ False,
+ "json",
+ models.UpdateDomainRestrictionsRequestBody,
+ ),
+ timeout_ms=timeout_ms,
+ ))
+
+ if retries == UNSET and self.sdk_configuration.retry_config is not UNSET:
+ retries = self.sdk_configuration.retry_config
+
+ retry_config = None
+ if isinstance(retries, utils.RetryConfig):
+ retry_config = (retries, ["429", "500", "502", "503", "504"])
+
+ http_res = await self.do_request_async(
+ hook_ctx=HookContext(
+ config=self.sdk_configuration,
+ base_url=base_url or "",
+ operation_id="update-domain-restrictions",
+ oauth2_scopes=None,
+ security_source=get_security_from_env(
+ self.sdk_configuration.security, models.Security
+ ),
+ ),
+ request=req,
+ error_status_codes=["4XX", "5XX"],
+ retry_config=retry_config,
+ )
+
+ if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(
+ models.UpdateDomainRestrictionsResponseBody, http_res
+ )
+ if utils.match_response(http_res, "4XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "5XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "default", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(models.DefaultError, http_res)
+
+ raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
+
+ async def update_user_agent_restrictions_async(
+ self,
+ *,
+ media_id: str,
+ playback_id: str,
+ default_policy: Optional[
+ models.UpdateUserAgentRestrictionsDefaultPolicy
+ ] = "allow",
+ allow: Optional[List[str]] = None,
+ deny: Optional[List[str]] = None,
+ retries: OptionalNullable[utils.RetryConfig] = UNSET,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> models.UpdateUserAgentRestrictionsResponseBody:
+ r"""Update user-agent restrictions for a playback ID
+
+ This endpoint allows updating user-agent restrictions for a specific playback ID associated with a media asset.
+ It can be used to allow or deny specific user-agents during playback request evaluation.
+
+ **How it works:**
+ 1. Make a `PATCH` request to this endpoint with your desired user-agent access configuration.
+ 2. Specify a default policy (`allow` or `deny`) and provide specific `allow` or `deny` lists.
+ 3. Use this to restrict access to specific browsers, devices, or bots.
+
+ **Example:**
+ A developer may configure a playback ID to deny access from known scraping user-agents while allowing all others by default.
+
+
+ :param media_id:
+ :param playback_id:
+ :param default_policy: The default behavior when a user-agent is not listed in `allow` or `deny`.
+ :param allow: List of user-agent substrings explicitly allowed.
+ :param deny: List of user-agent substrings explicitly denied.
+ :param retries: Override the default retry configuration for this method
+ :param server_url: Override the default server URL for this method
+ :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
+ :param http_headers: Additional headers to set or replace on requests.
+ """
+ base_url = None
+ url_variables = None
+ if timeout_ms is None:
+ timeout_ms = self.sdk_configuration.timeout_ms
+
+ if server_url is not None:
+ base_url = server_url
+ else:
+ base_url = self._get_url(base_url, url_variables)
+
+ request = models.UpdateUserAgentRestrictionsRequest(
+ media_id=media_id,
+ playback_id=playback_id,
+ body=models.UpdateUserAgentRestrictionsRequestBody(
+ default_policy=default_policy,
+ allow=allow,
+ deny=deny,
+ ),
+ )
+
+ req = self._build_request_async(BuildRequestData(
+ method="PATCH",
+ path="/on-demand/{mediaId}/playback-ids/{playbackId}/user-agents",
+ base_url=base_url,
+ url_variables=url_variables,
+ request=request,
+ request_body_required=True,
+ request_has_path_params=True,
+ request_has_query_params=True,
+ user_agent_header="user-agent",
+ accept_header_value=CONTENT_TYPE_JSON,
+ http_headers=http_headers,
+ security=self.sdk_configuration.security,
+ get_serialized_body=lambda: utils.serialize_request_body(
+ request.body,
+ False,
+ False,
+ "json",
+ models.UpdateUserAgentRestrictionsRequestBody,
+ ),
+ timeout_ms=timeout_ms,
+ ))
+
+ if retries == UNSET and self.sdk_configuration.retry_config is not UNSET:
+ retries = self.sdk_configuration.retry_config
+
+ retry_config = None
+ if isinstance(retries, utils.RetryConfig):
+ retry_config = (retries, ["429", "500", "502", "503", "504"])
+
+ http_res = await self.do_request_async(
+ hook_ctx=HookContext(
+ config=self.sdk_configuration,
+ base_url=base_url or "",
+ operation_id="update-user-agent-restrictions",
+ oauth2_scopes=None,
+ security_source=get_security_from_env(
+ self.sdk_configuration.security, models.Security
+ ),
+ ),
+ request=req,
+ error_status_codes=["4XX", "5XX"],
+ retry_config=retry_config,
+ )
+
+ if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(
+ models.UpdateUserAgentRestrictionsResponseBody, http_res
+ )
+ if utils.match_response(http_res, "4XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "5XX", "*"):
+ http_res_text = await utils.stream_to_text_async(http_res)
+ raise errors.FastpixDefaultError(
+ API_ERROR_MESSAGE, http_res, http_res_text
+ )
+ if utils.match_response(http_res, "default", CONTENT_TYPE_JSON):
+ return unmarshal_json_response(models.DefaultError, http_res)
+
+ raise errors.FastpixDefaultError(UNEXPECTED_RESPONSE_MESSAGE, http_res)
diff --git a/fastpix_python/playlist.py b/fastpix_python/playlist.py
index 68e466d..5029849 100644
--- a/fastpix_python/playlist.py
+++ b/fastpix_python/playlist.py
@@ -59,7 +59,7 @@ def create_a_playlist(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistCreatedSchema:
+ ) -> models.PlaylistCreatedResponse:
r"""Create a new playlist
This endpoint creates a new playlist within a specified workspace. A playlist acts as a container for organizing media items either manually or based on filters and metadata.
@@ -189,7 +189,7 @@ async def create_a_playlist_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistCreatedSchema:
+ ) -> models.PlaylistCreatedResponse:
r"""Create a new playlist
This endpoint creates a new playlist within a specified workspace. A playlist acts as a container for organizing media items either manually or based on filters and metadata.
@@ -309,7 +309,7 @@ def get_all_playlists(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.PlaylistItem]:
+ ) -> models.GetAllPlaylistsResponse:
r"""Get all playlists
This endpoint retrieves all playlists present within a specified workspace. It allows users to view the collection of playlists that have been created, whether manual or smart, along with their associated metadata.
@@ -404,7 +404,7 @@ async def get_all_playlists_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.PlaylistItem]:
+ ) -> models.GetAllPlaylistsResponse:
r"""Get all playlists
This endpoint retrieves all playlists present within a specified workspace. It allows users to view the collection of playlists that have been created, whether manual or smart, along with their associated metadata.
@@ -498,7 +498,7 @@ def get_playlist_by_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Get a playlist by ID
This endpoint retrieves detailed information about a specific playlist using its unique `playlistId`. It provides comprehensive metadata about the playlist, including its title, creation mode (manual or smart), media items along with the metadata of each media in the playlist.
@@ -589,7 +589,7 @@ async def get_playlist_by_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Get a playlist by ID
This endpoint retrieves detailed information about a specific playlist using its unique `playlistId`. It provides comprehensive metadata about the playlist, including its title, creation mode (manual or smart), media items along with the metadata of each media in the playlist.
@@ -682,7 +682,7 @@ def update_a_playlist(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistCreatedSchema:
+ ) -> models.PlaylistCreatedResponse:
r"""Update a playlist by ID
This endpoint allows you to update the name and description of an existing playlist. It enables modifications to the playlist's metadata without altering the media items or playlist structure.
@@ -792,7 +792,7 @@ async def update_a_playlist_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistCreatedSchema:
+ ) -> models.PlaylistCreatedResponse:
r"""Update a playlist by ID
This endpoint allows you to update the name and description of an existing playlist. It enables modifications to the playlist's metadata without altering the media items or playlist structure.
@@ -900,7 +900,7 @@ def delete_a_playlist(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.SuccessResponseData]:
+ ) -> models.SuccessResponse:
r"""Delete a playlist by ID
This endpoint allows you to delete an existing playlist from the workspace. Once deleted, the playlist and its metadata are permanently removed and cannot be recovered.
@@ -993,7 +993,7 @@ async def delete_a_playlist_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.SuccessResponseData]:
+ ) -> models.SuccessResponse:
r"""Delete a playlist by ID
This endpoint allows you to delete an existing playlist from the workspace. Once deleted, the playlist and its metadata are permanently removed and cannot be recovered.
@@ -1087,7 +1087,7 @@ def add_media_to_playlist(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Add media to a playlist by ID
This endpoint allows you to add one or more media items to an existing playlist. By passing the media ID(s) in the request, the specified media items are appended to the playlist in the order provided.
@@ -1191,7 +1191,7 @@ async def add_media_to_playlist_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Add media to a playlist by ID
This endpoint allows you to add one or more media items to an existing playlist. By passing the media ID(s) in the request, the specified media items are appended to the playlist in the order provided.
@@ -1295,7 +1295,7 @@ def change_media_order_in_playlist(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Change media order in a playlist by ID
This endpoint allows you to change the order of media items within a playlist. By passing the complete list of media IDs in the desired sequence, the playlist's play order is updated accordingly.
@@ -1397,7 +1397,7 @@ async def change_media_order_in_playlist_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Change media order in a playlist by ID
This endpoint allows you to change the order of media items within a playlist. By passing the complete list of media IDs in the desired sequence, the playlist's play order is updated accordingly.
@@ -1499,7 +1499,7 @@ def delete_media_from_playlist(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Delete media in a playlist by ID
This endpoint allows you to delete one or more media items from an existing playlist. By passing the media ID(s) in the request, the specified media items are removed from the playlist.
@@ -1605,7 +1605,7 @@ async def delete_media_from_playlist_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.PlaylistByIDResponseData:
+ ) -> models.PlaylistByIDResponse:
r"""Delete media in a playlist by ID
This endpoint allows you to delete one or more media items from an existing playlist. By passing the media ID(s) in the request, the specified media items are removed from the playlist.
diff --git a/fastpix_python/signing_keys.py b/fastpix_python/signing_keys.py
index 5ed69f6..8696bb7 100644
--- a/fastpix_python/signing_keys.py
+++ b/fastpix_python/signing_keys.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import List, Mapping, NoReturn, Optional
+from typing import Mapping, NoReturn, Optional
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -47,7 +47,7 @@ def create_signing_key(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.CreateSigningKeyResponseDTO:
+ ) -> models.CreateResponse:
r"""Create a signing key
This endpoint allows you to create a new signing key pair for FastPix. When you call this endpoint, the API generates a 2048-bit RSA key pair. The privateKey will be returned in the response, encoded in Base64 format, and you will receive a unique key id to reference the key in future operations. FastPix will securely store the public key to validate signed tokens.
@@ -151,7 +151,7 @@ async def create_signing_key_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.CreateSigningKeyResponseDTO:
+ ) -> models.CreateResponse:
r"""Create a signing key
This endpoint allows you to create a new signing key pair for FastPix. When you call this endpoint, the API generates a 2048-bit RSA key pair. The privateKey will be returned in the response, encoded in Base64 format, and you will receive a unique key id to reference the key in future operations. FastPix will securely store the public key to validate signed tokens.
@@ -240,7 +240,7 @@ async def create_signing_key_async(
if utils.match_response(http_res, "201", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.CreateResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnAuthorizedResponseErrorData, errors.UnAuthorizedResponseError),
@@ -459,7 +459,7 @@ async def list_signing_keys_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.GetAllSigningKeyResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnAuthorizedResponseErrorData, errors.UnAuthorizedResponseError),
@@ -670,7 +670,7 @@ async def delete_signing_key_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.DeleteSigningKeyResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnAuthorizedResponseErrorData, errors.UnAuthorizedResponseError),
@@ -688,7 +688,7 @@ def get_signing_key_by_id(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetPublicPemUsingSigningKeyIDResponseDTOData:
+ ) -> models.GetPublicPemUsingSigningKeyIDResponseDTO:
r"""Get signing key by ID
This endpoint allows you to retrieve detailed information about a specific signing key using its unique key id. While the private key is not returned for security reasons, you'll be able to see the key's creation date, status, and other associated metadata. This endpoint also returns the workspaceId and publicKey in the response.
@@ -824,7 +824,7 @@ async def get_signing_key_by_id_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetPublicPemUsingSigningKeyIDResponseDTOData:
+ ) -> models.GetPublicPemUsingSigningKeyIDResponseDTO:
r"""Get signing key by ID
This endpoint allows you to retrieve detailed information about a specific signing key using its unique key id. While the private key is not returned for security reasons, you'll be able to see the key's creation date, status, and other associated metadata. This endpoint also returns the workspaceId and publicKey in the response.
@@ -942,7 +942,7 @@ async def get_signing_key_by_id_async(
return unmarshal_json_response(
models.GetPublicPemUsingSigningKeyIDResponseDTO, http_res
)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnAuthorizedResponseErrorData, errors.UnAuthorizedResponseError),
diff --git a/fastpix_python/simulcast_stream.py b/fastpix_python/simulcast_stream.py
index 185c7ec..8c54c21 100644
--- a/fastpix_python/simulcast_stream.py
+++ b/fastpix_python/simulcast_stream.py
@@ -50,7 +50,7 @@ def create_simulcast_of_stream(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SimulcastResponseData:
+ ) -> models.SimulcastResponse:
r"""Create a simulcast
Lets you to create a simulcast for a parent live stream. Simulcasting enables you to broadcast the live stream to multiple social platforms simultaneously (e.g., YouTube, Facebook, or Twitch). This feature is useful for expanding your audience reach across different platforms. However, a simulcast can only be created when the parent live stream is in idle state (i.e., not currently live or disabled). Additionally, only one simulcast target can be created per API call.
@@ -163,7 +163,7 @@ async def create_simulcast_of_stream_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SimulcastResponseData:
+ ) -> models.SimulcastResponse:
r"""Create a simulcast
Lets you to create a simulcast for a parent live stream. Simulcasting enables you to broadcast the live stream to multiple social platforms simultaneously (e.g., YouTube, Facebook, or Twitch). This feature is useful for expanding your audience reach across different platforms. However, a simulcast can only be created when the parent live stream is in idle state (i.e., not currently live or disabled). Additionally, only one simulcast target can be created per API call.
@@ -254,7 +254,7 @@ async def create_simulcast_of_stream_async(
if utils.match_response(http_res, "201", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.SimulcastResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("400", errors.SimulcastUnavailableErrorData, errors.SimulcastUnavailableError),
@@ -449,7 +449,7 @@ async def delete_simulcast_of_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.SimulcastdeleteResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -468,7 +468,7 @@ def get_specific_simulcast_of_stream(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SimulcastResponseData:
+ ) -> models.SimulcastResponse:
r"""Get a specific simulcast
Retrieves the details of a specific simulcast associated with a parent live stream. By providing both the `streamId` of the parent stream and the `simulcastId`, FastPix returns detailed information about the simulcast, such as the stream URL, the status of the simulcast, and metadata.
@@ -562,7 +562,7 @@ async def get_specific_simulcast_of_stream_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SimulcastResponseData:
+ ) -> models.SimulcastResponse:
r"""Get a specific simulcast
Retrieves the details of a specific simulcast associated with a parent live stream. By providing both the `streamId` of the parent stream and the `simulcastId`, FastPix returns detailed information about the simulcast, such as the stream URL, the status of the simulcast, and metadata.
@@ -637,7 +637,7 @@ async def get_specific_simulcast_of_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.SimulcastResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
@@ -658,7 +658,7 @@ def update_specific_simulcast_of_stream(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SimulcastUpdateResponseData:
+ ) -> models.SimulcastUpdateResponse:
r"""Update a simulcast
Allows you to enable or disable a specific simulcast associated with a parent live stream. The status of the simulcast can be updated at any point, whether the live stream is active or idle. However, once the live stream is disabled, the simulcast can no longer be modified.
@@ -769,7 +769,7 @@ async def update_specific_simulcast_of_stream_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.SimulcastUpdateResponseData:
+ ) -> models.SimulcastUpdateResponse:
r"""Update a simulcast
Allows you to enable or disable a specific simulcast associated with a parent live stream. The status of the simulcast can be updated at any point, whether the live stream is active or idle. However, once the live stream is disabled, the simulcast can no longer be modified.
@@ -859,7 +859,7 @@ async def update_specific_simulcast_of_stream_async(
if utils.match_response(http_res, "200", CONTENT_TYPE_JSON):
return unmarshal_json_response(models.SimulcastUpdateResponse, http_res)
- self._raise_for_status_async(
+ await self._raise_for_status_async(
http_res,
[
("401", errors.UnauthorizedErrorData, errors.UnauthorizedError),
diff --git a/fastpix_python/start_live_stream.py b/fastpix_python/start_live_stream.py
index ba7c089..e2d2efc 100644
--- a/fastpix_python/start_live_stream.py
+++ b/fastpix_python/start_live_stream.py
@@ -27,7 +27,7 @@ def create_new_stream(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetCreateLiveStreamResponseDTO:
+ ) -> models.LiveStreamResponseDTO:
r"""Create a new stream
Allows you to initiate a new RTMPS or SRT live stream on FastPix. Upon creating a stream, FastPix generates a unique `streamKey` and `srtSecret`, which can be used with any broadcasting software (like OBS) to connect to FastPix's RTMPS or SRT servers.
@@ -166,7 +166,7 @@ async def create_new_stream_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.GetCreateLiveStreamResponseDTO:
+ ) -> models.LiveStreamResponseDTO:
r"""Create a new stream
Allows you to initiate a new RTMPS or SRT live stream on FastPix. Upon creating a stream, FastPix generates a unique `streamKey` and `srtSecret`, which can be used with any broadcasting software (like OBS) to connect to FastPix's RTMPS or SRT servers.
diff --git a/fastpix_python/views_sdk.py b/fastpix_python/views_sdk.py
index 255ef0c..aff40fe 100644
--- a/fastpix_python/views_sdk.py
+++ b/fastpix_python/views_sdk.py
@@ -6,7 +6,7 @@
from .types import OptionalNullable, UNSET
from .utils import get_security_from_env
from .utils.unmarshal_json_response import unmarshal_json_response
-from typing import List, Mapping, NoReturn, Optional, Union
+from typing import Mapping, NoReturn, Optional, Union
CONTENT_TYPE_JSON = "application/json"
API_ERROR_MESSAGE = "API error occurred"
@@ -54,7 +54,7 @@ def list_video_views(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.ViewsList]:
+ ) -> models.ListVideoViewsResponse:
r"""List video views
Retrieves a list of video views that fall within the specified filters and have been completed within a defined timespan. It allows you to analyse viewer interactions with your video content effectively.
@@ -189,7 +189,7 @@ async def list_video_views_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.ViewsList]:
+ ) -> models.ListVideoViewsResponse:
r"""List video views
Retrieves a list of video views that fall within the specified filters and have been completed within a defined timespan. It allows you to analyse viewer interactions with your video content effectively.
@@ -315,7 +315,7 @@ def get_video_view_details(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Views:
+ ) -> models.GetVideoViewDetailsResponse:
r"""Get details of video view
Allows you to retrieve detailed information about a specific video view using its unique `viewId`. This is useful for getting insights into individual viewer interactions with your video content. This detailed information is valuable for enhancing user experience and improving engagement with your video assets.
@@ -412,7 +412,7 @@ async def get_video_view_details_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> models.Views:
+ ) -> models.GetVideoViewDetailsResponse:
r"""Get details of video view
Allows you to retrieve detailed information about a specific video view using its unique `viewId`. This is useful for getting insights into individual viewer interactions with your video content. This detailed information is valuable for enhancing user experience and improving engagement with your video assets.
@@ -511,7 +511,7 @@ def list_by_top_content(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.ViewsByTopContentDetails]:
+ ) -> models.ListByTopContentResponse:
r"""List by top content
Retrieves a list of the top video views that fall within the specified filters and have been completed within a defined timespan. It allows you to identify the most popular content based on viewer interactions.
@@ -620,7 +620,7 @@ async def list_by_top_content_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.ViewsByTopContentDetails]:
+ ) -> models.ListByTopContentResponse:
r"""List by top content
Retrieves a list of the top video views that fall within the specified filters and have been completed within a defined timespan. It allows you to identify the most popular content based on viewer interactions.
@@ -726,7 +726,7 @@ def get_data_viewlist_current_views_get_timeseries_views(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.GetDataViewlistCurrentViewsGetTimeseriesViewsData]:
+ ) -> models.GetDataViewlistCurrentViewsGetTimeseriesViewsResponse:
r"""Get concurrent viewers timeseries
Retrieves a time series of the number of concurrent viewers, providing a real-time snapshot of audience activity over the last 30 minutes. This endpoint is essential for monitoring live events, gauging audience reaction to new content releases, or understanding immediate engagement trends.
@@ -822,7 +822,7 @@ async def get_data_viewlist_current_views_get_timeseries_views_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.GetDataViewlistCurrentViewsGetTimeseriesViewsData]:
+ ) -> models.GetDataViewlistCurrentViewsGetTimeseriesViewsResponse:
r"""Get concurrent viewers timeseries
Retrieves a time series of the number of concurrent viewers, providing a real-time snapshot of audience activity over the last 30 minutes. This endpoint is essential for monitoring live events, gauging audience reaction to new content releases, or understanding immediate engagement trends.
@@ -920,7 +920,7 @@ def get_data_viewlist_current_views_filter(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.GetDataViewlistCurrentViewsFilterData]:
+ ) -> models.GetDataViewlistCurrentViewsFilterResponse:
r"""Get concurrent viewers breakdown by dimension
Retrieves a real-time breakdown of present concurrent viewers, grouped by a chosen dimension. This endpoint allows you to see how your audience is distributed across different categories like geography, content, or technology, based on activity in the last 30 minutes.
@@ -1030,7 +1030,7 @@ async def get_data_viewlist_current_views_filter_async(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
- ) -> List[models.GetDataViewlistCurrentViewsFilterData]:
+ ) -> models.GetDataViewlistCurrentViewsFilterResponse:
r"""Get concurrent viewers breakdown by dimension
Retrieves a real-time breakdown of present concurrent viewers, grouped by a chosen dimension. This endpoint allows you to see how your audience is distributed across different categories like geography, content, or technology, based on activity in the last 30 minutes.
diff --git a/package-lock.json b/package-lock.json
index 5c96e3b..11dda2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,471 +6,8 @@
"": {
"name": "fastpix-python-sdk-devtools",
"devDependencies": {
- "@types/js-yaml": "^4.0.9",
- "@types/node": "^25.0.2",
"js-yaml": "^4.1.1",
- "openapi-response-validator": "^12.1.3",
- "tsx": "^4.21.0",
- "typescript": "~5.8.3"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
- "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
- "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
- "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
- "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
- "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
- "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
- "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
- "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
- "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
- "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
- "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
- "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
- "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
- "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
- "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
- "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
- "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
- "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
- "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
- "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
- "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
- "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
- "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
- "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@types/js-yaml": {
- "version": "4.0.9",
- "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
- "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "25.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
- "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
+ "openapi-response-validator": "^12.1.3"
}
},
"node_modules/ajv": {
@@ -497,48 +34,6 @@
"dev": true,
"license": "Python-2.0"
},
- "node_modules/esbuild": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
- "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.0",
- "@esbuild/android-arm": "0.28.0",
- "@esbuild/android-arm64": "0.28.0",
- "@esbuild/android-x64": "0.28.0",
- "@esbuild/darwin-arm64": "0.28.0",
- "@esbuild/darwin-x64": "0.28.0",
- "@esbuild/freebsd-arm64": "0.28.0",
- "@esbuild/freebsd-x64": "0.28.0",
- "@esbuild/linux-arm": "0.28.0",
- "@esbuild/linux-arm64": "0.28.0",
- "@esbuild/linux-ia32": "0.28.0",
- "@esbuild/linux-loong64": "0.28.0",
- "@esbuild/linux-mips64el": "0.28.0",
- "@esbuild/linux-ppc64": "0.28.0",
- "@esbuild/linux-riscv64": "0.28.0",
- "@esbuild/linux-s390x": "0.28.0",
- "@esbuild/linux-x64": "0.28.0",
- "@esbuild/netbsd-arm64": "0.28.0",
- "@esbuild/netbsd-x64": "0.28.0",
- "@esbuild/openbsd-arm64": "0.28.0",
- "@esbuild/openbsd-x64": "0.28.0",
- "@esbuild/openharmony-arm64": "0.28.0",
- "@esbuild/sunos-x64": "0.28.0",
- "@esbuild/win32-arm64": "0.28.0",
- "@esbuild/win32-ia32": "0.28.0",
- "@esbuild/win32-x64": "0.28.0"
- }
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -563,21 +58,6 @@
],
"license": "BSD-3-Clause"
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -625,46 +105,6 @@
"engines": {
"node": ">=0.10.0"
}
- },
- "node_modules/tsx": {
- "version": "4.22.3",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz",
- "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.28.0"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
- "node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "dev": true,
- "license": "MIT"
}
}
}
diff --git a/package.json b/package.json
index faa1791..4feb36d 100644
--- a/package.json
+++ b/package.json
@@ -2,15 +2,8 @@
"name": "fastpix-python-sdk-devtools",
"private": true,
"type": "module",
- "scripts": {
- "validate:get-endpoints": "tsx tests/validate-get-endpoints.ts"
- },
"devDependencies": {
- "@types/js-yaml": "^4.0.9",
- "@types/node": "^25.0.2",
"js-yaml": "^4.1.1",
- "openapi-response-validator": "^12.1.3",
- "tsx": "^4.21.0",
- "typescript": "~5.8.3"
+ "openapi-response-validator": "^12.1.3"
}
}
diff --git a/pyproject.toml b/pyproject.toml
index e31ed83..cca4e7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "fastpix_python"
-version = "1.1.5"
+version = "1.2.0"
description = "Python Client SDK Generated by fastpix."
authors = [
{ name = "FastPix", email = "devs@fastpix.com" }
@@ -22,6 +22,7 @@ dev = [
"jsonschema >=4.0.0",
"pyyaml >=6.0",
"pytest >=8.0",
+ "pytest-asyncio >=0.23",
]
[tool.setuptools.packages.find]
diff --git a/tests/.env.example b/tests/.env.example
new file mode 100644
index 0000000..afe28de
--- /dev/null
+++ b/tests/.env.example
@@ -0,0 +1,7 @@
+# FastPix API credentials (from the FastPix dashboard).
+# Copy this file to `.env` and fill in your values.
+FASTPIX_USERNAME=your-access-token-id
+FASTPIX_PASSWORD=your-secret-key
+
+# Optional: override the API base URL (defaults to the URL in the OpenAPI spec).
+# FASTPIX_BASE_URL=https://api.fastpix.com/v1
diff --git a/tests/README.md b/tests/README.md
index dcf3ee4..be5f13b 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,226 +1,54 @@
-# GET Endpoints Validation (Hybrid: OpenAPI via Node, SDK via Python)
+# Tests
-## Quick Start
+Two layers: fast offline pytest suites, and a live validation harness that
+exercises every endpoint against a real workspace.
-1. Install Node deps:
+## Offline tests (no credentials needed)
```bash
-cd fastpix-python
-npm install
+pip install -e . pytest
+pytest tests/test_models.py tests/test_async_errors.py tests/test_return_annotations.py tests/test_examples.py
```
-2. Set env vars:
-
-```bash
-export FASTPIX_USERNAME="your-username"
-export FASTPIX_PASSWORD="your-password"
-# optional:
-# export FASTPIX_BASE_URL="https://api.fastpix.com/v1/"
-```
+- `test_models.py` — model contract tests (field types, aliases, defaults,
+ serialized body shapes). Pure pydantic, no network.
+- `test_async_errors.py` — mocked-transport checks that sync and async methods
+ raise typed errors on failed responses.
+- `test_return_annotations.py` — asserts every resource method's declared return
+ type is the response class it actually unmarshals.
+- `test_examples.py` — sanity checks on `examples/` (compiles, has a
+ `__main__` guard, no hardcoded credentials).
-3. Run:
+## Live validation harness
-```bash
-cd fastpix-python
-npm run validate:get-endpoints
-```
+Calls every endpoint through the SDK against a real FastPix workspace and
+validates each response against the OpenAPI spec.
-Artifacts and reports are written into `fastpix-python/tests/`.
+### Setup
-
-Last generated: 2026-01-23T12:54:02.880Z
+1. Copy `tests/.env.example` to `tests/.env` and fill in your credentials.
+2. Place the OpenAPI spec at the repo root as `openapi.yaml`.
+3. Install dependencies (the response validator runs in a small Node sidecar):
-- **Total GET endpoints**: 30
-- **PASS**: 21
-- **FAIL**: 9
-- **SKIP**: 0
+```bash
+pip install -e . httpx pyyaml
+npm install
+```
-| Endpoint | OperationId | OpenAPI valid | SDK parse | Missing in SDK (present in API) | Missing in API (present in SDK) | Empty arrays omitted by SDK | Status |
-|---|---|---:|---:|---|---|---|---|
-| `/on-demand` | `list-media` | ❌ | ✅ | `data[].tracks[].frameRate` | None | None | ❌ FAIL |
-| `/on-demand/{livestreamId}/live-clips` | `list-live-clips` | ❌ | ✅ | None | None | None | ❌ FAIL |
-| `/on-demand/{mediaId}` | `get-media` | ❌ | ✅ | None | None | None | ❌ FAIL |
-| `/on-demand/{mediaId}/summary` | `get-media-summary` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/on-demand/{mediaId}/input-info` | `retrieveMediaInputInfo` | ❌ | ✅ | None | None | None | ❌ FAIL |
-| `/on-demand/{mediaId}/playback-ids` | `list-playback-ids` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/on-demand/uploads` | `list-uploads` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/on-demand/{mediaId}/media-clips` | `get-media-clips` | ✅ | ❌ | None | None | `data` | ❌ FAIL |
-| `/on-demand/playlists` | `get-all-playlists` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/on-demand/playlists/{playlistId}` | `get-playlist-by-id` | ✅ | ✅ | `data.createdAt`, `data.mediaCount`, `data.referenceId`, `data.updatedAt`, `data.workspaceId` | None | `data.mediaList` | ❌ FAIL |
-| `/on-demand/{mediaId}/playback-ids/{playbackId}` | `get-playback-id` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/on-demand/drm-configurations` | `getDrmConfiguration` | ✅ | ❌ | None | None | None | ❌ FAIL |
-| `/on-demand/drm-configurations/{drmConfigurationId}` | `getDrmConfigurationById` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/live/streams` | `get-all-streams` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/live/streams/{streamId}/viewer-count` | `get-live-stream-viewer-count-by-id` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/live/streams/{streamId}` | `get-live-stream-by-id` | ✅ | ❌ | None | None | None | ❌ FAIL |
-| `/live/streams/{streamId}/playback-ids/{playbackId}` | `get-live-stream-playback-id` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/live/streams/{streamId}/simulcast/{simulcastId}` | `get-specific-simulcast-of-stream` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/iam/signing-keys` | `list_signing_keys` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/iam/signing-keys/{signingKeyId}` | `get-signing_key_by_id` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/viewlist` | `list_video_views` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/viewlist/{viewId}` | `get_video_view_details` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/viewlist/top-content` | `list_by_top_content` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/dimensions` | `list_dimensions` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/dimensions/{dimensionsId}` | `list_filter_values_for_dimension` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/metrics/{metricId}/breakdown` | `list_breakdown_values` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/metrics/{metricId}/overall` | `list_overall_values` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/metrics/{metricId}/timeseries` | `get_timeseries_data` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/metrics/comparison` | `list_comparison_values` | ✅ | ✅ | None | None | None | ✅ PASS |
-| `/data/errors` | `list_errors` | ✅ | ❌ | None | None | `data.errors`, `data.topErrors` | ❌ FAIL |
+### Run
-#### Missing fields (full lists)
+```bash
+set -a; source tests/.env; set +a
+python -m tests.validate_get_endpoints # read-only endpoints
+python -m tests.validate_non_get_endpoints # creates, updates, deletes (cleans up after itself)
+```
-- **list-media** (`/on-demand`)
- - **Missing in SDK (present in API)**: `data[].tracks[].frameRate`
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list-live-clips** (`/on-demand/{livestreamId}/live-clips`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-media** (`/on-demand/{mediaId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-media-summary** (`/on-demand/{mediaId}/summary`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **retrieveMediaInputInfo** (`/on-demand/{mediaId}/input-info`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list-playback-ids** (`/on-demand/{mediaId}/playback-ids`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list-uploads** (`/on-demand/uploads`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-media-clips** (`/on-demand/{mediaId}/media-clips`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: `data`
- - **Empty arrays omitted by API**: None
-- **get-all-playlists** (`/on-demand/playlists`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-playlist-by-id** (`/on-demand/playlists/{playlistId}`)
- - **Missing in SDK (present in API)**: `data.createdAt`, `data.mediaCount`, `data.referenceId`, `data.updatedAt`, `data.workspaceId`
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: `data.mediaList`
- - **Empty arrays omitted by API**: None
-- **get-playback-id** (`/on-demand/{mediaId}/playback-ids/{playbackId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **getDrmConfiguration** (`/on-demand/drm-configurations`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **getDrmConfigurationById** (`/on-demand/drm-configurations/{drmConfigurationId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-all-streams** (`/live/streams`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-live-stream-viewer-count-by-id** (`/live/streams/{streamId}/viewer-count`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-live-stream-by-id** (`/live/streams/{streamId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-live-stream-playback-id** (`/live/streams/{streamId}/playback-ids/{playbackId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-specific-simulcast-of-stream** (`/live/streams/{streamId}/simulcast/{simulcastId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_signing_keys** (`/iam/signing-keys`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get-signing_key_by_id** (`/iam/signing-keys/{signingKeyId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_video_views** (`/data/viewlist`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get_video_view_details** (`/data/viewlist/{viewId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_by_top_content** (`/data/viewlist/top-content`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_dimensions** (`/data/dimensions`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_filter_values_for_dimension** (`/data/dimensions/{dimensionsId}`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_breakdown_values** (`/data/metrics/{metricId}/breakdown`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_overall_values** (`/data/metrics/{metricId}/overall`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **get_timeseries_data** (`/data/metrics/{metricId}/timeseries`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_comparison_values** (`/data/metrics/comparison`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: None
- - **Empty arrays omitted by API**: None
-- **list_errors** (`/data/errors`)
- - **Missing in SDK (present in API)**: None
- - **Missing in API (present in SDK)**: None
- - **Empty arrays omitted by SDK**: `data.errors`, `data.topErrors`
- - **Empty arrays omitted by API**: None
+Each run writes a markdown report and per-endpoint response artifacts into
+`tests/` (gitignored). Endpoints needing existing resource IDs read them from
+`tests/get_endpoints_fixtures.json`; non-GET request bodies come from
+`tests/non_get_endpoints_fixtures.json`.
-Full details: `tests/GET_ENDPOINTS_OPENAPI_RESPONSE_VALIDATION_REPORT.md`
-
+## Utilities
+- `check_broken_links.py` — verifies every external URL in the repo's markdown
+ docs resolves (`pip install httpx`, then `python tests/check_broken_links.py`).
diff --git a/tests/_common.py b/tests/_common.py
index 0f3c947..cf32f8b 100644
--- a/tests/_common.py
+++ b/tests/_common.py
@@ -12,7 +12,7 @@
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
-SPEC_PATH = REPO_ROOT / "fixed.yaml"
+SPEC_PATH = REPO_ROOT / "openapi.yaml"
ARTIFACTS_GET = REPO_ROOT / "tests" / "artifacts"
ARTIFACTS_NON_GET = REPO_ROOT / "tests" / "artifacts_non_get"
PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000"
@@ -46,11 +46,20 @@ def load_spec(path: Path = SPEC_PATH) -> Dict[str, Any]:
raise SystemExit(
"PyYAML is required. Install with `pip install pyyaml`."
) from exc
+ if not path.exists():
+ raise SystemExit(
+ f"{path.name} not found at {path}. The validators need a local, "
+ "untracked snapshot of the FastPix OpenAPI spec at this path to "
+ "validate API responses against."
+ )
with path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f)
def spec_server_url(spec: Mapping[str, Any]) -> str:
+ override = os.environ.get("FASTPIX_BASE_URL")
+ if override:
+ return override.rstrip("/")
servers = spec.get("servers") or []
if not servers:
raise RuntimeError("Spec has no servers[] entry.")
@@ -533,6 +542,9 @@ def build_sdk(client: Optional[Any] = None) -> Any:
kwargs: Dict[str, Any] = {"security": security}
if client is not None:
kwargs["client"] = client
+ base_override = os.environ.get("FASTPIX_BASE_URL")
+ if base_override:
+ kwargs["server_url"] = base_override.rstrip("/")
return fastpix_cls(**kwargs)
diff --git a/tests/_reporting.py b/tests/_reporting.py
index ccb5644..97d8c8f 100644
--- a/tests/_reporting.py
+++ b/tests/_reporting.py
@@ -55,7 +55,7 @@ def _suggest_enum_oneof(r: EndpointResult, paths: List[str]) -> List[Dict[str, s
"`oneOf` requires exactly one match."
),
"where": (
- "In `fixed.yaml`: "
+ "In `openapi.yaml`: "
"`components/schemas/{VideoTrack,VideoTrackForGetAll,AudioTrack,SubtitleTrack}.properties.type`"
),
"paste_yaml": (
@@ -80,7 +80,7 @@ def _suggest_enum_oneof(r: EndpointResult, paths: List[str]) -> List[Dict[str, s
"`\"480p\"`-style values."
),
"where": (
- "In `fixed.yaml`: under the relevant media response schema(s) "
+ "In `openapi.yaml`: under the relevant media response schema(s) "
"`sourceResolution:` field definition"
),
})
@@ -96,7 +96,7 @@ def _suggest_enum_oneof(r: EndpointResult, paths: List[str]) -> List[Dict[str, s
"rejects them."
),
"where": (
- "In `fixed.yaml`: media response schemas' `maxResolution:` field definition"
+ "In `openapi.yaml`: media response schemas' `maxResolution:` field definition"
),
})
@@ -119,7 +119,7 @@ def _suggest_schema_overlap(r: EndpointResult, paths: List[str]) -> List[Dict[st
"multiple branches."
),
"where": (
- "In `fixed.yaml`: "
+ "In `openapi.yaml`: "
"`paths./data/dimensions.get.responses.200.content.application/json.schema.properties.data.oneOf`"
),
})
@@ -134,7 +134,7 @@ def _suggest_schema_overlap(r: EndpointResult, paths: List[str]) -> List[Dict[st
"In JSON Schema, `integer` is a subset of `number`. A value like `0` matches both, "
"causing oneOf validation errors."
),
- "where": "In `fixed.yaml`: metrics schemas that use `oneOf: [integer, number]`",
+ "where": "In `openapi.yaml`: metrics schemas that use `oneOf: [integer, number]`",
})
return out
@@ -149,7 +149,7 @@ def _suggest_field_issues(r: EndpointResult, paths: List[str]) -> List[Dict[str,
out.append({
"title": "Make `fpApiVersion` nullable in the spec",
"why": "The API can return `null` for fpApiVersion but the schema declares `string` only.",
- "where": "In `fixed.yaml`: `components/schemas/Views.properties.fpApiVersion`",
+ "where": "In `openapi.yaml`: `components/schemas/Views.properties.fpApiVersion`",
})
# 7) phantom Optional fields the API never returns
@@ -162,7 +162,7 @@ def _suggest_field_issues(r: EndpointResult, paths: List[str]) -> List[Dict[str,
"the spec (preferred) or surface them only when the API actually returns them."
),
"where": (
- "In `fixed.yaml`: response schemas for `" + r.operation_id + "`; "
+ "In `openapi.yaml`: response schemas for `" + r.operation_id + "`; "
"fields to inspect: " + ", ".join(f"`{p}`" for p in r.missing_in_api)
),
})
@@ -177,7 +177,7 @@ def _suggest_field_issues(r: EndpointResult, paths: List[str]) -> List[Dict[str,
),
"where": (
"In the generated SDK: response model for `" + r.operation_id + "`. "
- "Update the operation's response schema in `fixed.yaml` to the full envelope and regenerate."
+ "Update the operation's response schema in `openapi.yaml` to the full envelope and regenerate."
),
})
@@ -191,7 +191,7 @@ def _suggest_field_issues(r: EndpointResult, paths: List[str]) -> List[Dict[str,
"while the actual `metadata` payload is dropped."
),
"where": (
- "Either fix `fixed.yaml` to spell the field `metadata` everywhere, "
+ "Either fix `openapi.yaml` to spell the field `metadata` everywhere, "
"or add a wire-format alias in the SDK's response model."
),
})
diff --git a/tests/get-endpoints-fixtures.json b/tests/get-endpoints-fixtures.json
deleted file mode 100644
index 933dcaa..0000000
--- a/tests/get-endpoints-fixtures.json
+++ /dev/null
@@ -1,179 +0,0 @@
-{
- "operations": {
- "list-media": {
- "query": {
- "limit": 10,
- "offset": 1,
- "orderBy": "desc"
- }
- },
- "list-live-clips": {
- "pathParams": {
- "livestreamId": "your-livestream-id"
- }
- },
- "get-media": {
- "pathParams": {
- "mediaId": "your-media-id"
- }
- },
- "get-media-summary": {
- "pathParams": {
- "mediaId": "your-media-id"
- }
- },
- "retrieveMediaInputInfo": {
- "pathParams": {
- "mediaId": "your-media-id"
- }
- },
- "list-playback-ids": {
- "pathParams": {
- "mediaId": "your-media-id"
- }
- },
- "list-uploads": {
- "query": {
- "limit": 5,
- "offset": 1,
- "orderBy": "desc"
- }
- },
- "get-media-clips": {
- "pathParams": {
- "mediaId": "your-media-id"
- }
- },
- "get-all-playlists": {
- "query": {
- "limit": 5,
- "offset": 1
- }
- },
- "get-playback-id": {
- "pathParams": {
- "mediaId": "your-media-id",
- "playbackId": "your-playback-id"
- }
- },
- "get-playlist-by-id": {
- "pathParams": {
- "playlistId": "your-playlist-id"
- }
- },
- "getDrmConfiguration": {
- "query": {
- "limit": 10,
- "offset": 1
- }
- },
- "getDrmConfigurationById": {
- "pathParams": {
- "drmConfigurationId": "your-drm-configuration-id"
- }
- },
- "get-all-streams": {
- "query": {
- "limit": 5,
- "offset": 1,
- "orderBy": "desc"
- }
- },
- "get-live-stream-by-id": {
- "pathParams": {
- "streamId": "your-stream-id"
- }
- },
- "get-live-stream-viewer-count-by-id": {
- "pathParams": {
- "streamId": "your-stream-id"
- }
- },
- "get-live-stream-playback-id": {
- "pathParams": {
- "streamId": "your-stream-id",
- "playbackId": "your-playback-id"
- }
- },
- "get-specific-simulcast-of-stream": {
- "pathParams": {
- "streamId": "your-stream-id",
- "simulcastId": "your-simulcast-id"
- }
- },
- "list_signing_keys": {
- "query": {
- "limit": 5,
- "offset": 1
- }
- },
- "get-signing_key_by_id": {
- "pathParams": {
- "signingKeyId": "your-signing-key-id"
- }
- },
- "list_video_views": {
- "query": {
- "timespan": "24:hours",
- "limit": 5,
- "offset": 1
- }
- },
- "get_video_view_details": {
- "pathParams": {
- "viewId": "your-view-id"
- }
- },
- "list_by_top_content": {
- "query": {
- "timespan": "24:hours",
- "limit": 5
- }
- },
- "list_dimensions": {},
- "list_filter_values_for_dimension": {
- "pathParams": {
- "dimensionsId": "browser_name"
- }
- },
- "list_breakdown_values": {
- "pathParams": {
- "metricId": "quality_of_experience_score"
- },
- "query": {
- "timespan": "24:hours",
- "groupBy": "browser_name"
- }
- },
- "list_overall_values": {
- "pathParams": {
- "metricId": "quality_of_experience_score"
- },
- "query": {
- "timespan": "24:hours"
- }
- },
- "get_timeseries_data": {
- "pathParams": {
- "metricId": "quality_of_experience_score"
- },
- "query": {
- "timespan": "24:hours",
- "groupBy": "hour"
- }
- },
- "list_comparison_values": {
- "query": {
- "timespan": "24:hours",
- "dimension": "browser_name",
- "value": "Chrome"
- }
- },
- "list_errors": {
- "query": {
- "timespan": "24:hours",
- "limit": 5
- }
- }
- }
-}
diff --git a/tests/migrate_doc_links.py b/tests/migrate_doc_links.py
deleted file mode 100644
index 190393a..0000000
--- a/tests/migrate_doc_links.py
+++ /dev/null
@@ -1,270 +0,0 @@
-"""Rewrite every stale FastPix doc URL in the repo to match ``fixed.yaml``.
-
-Background: ``old-yaml.yaml`` shipped doc links pointing at
-``https://docs.fastpix.io/...``. A naive ``.io → .com`` host swap landed across
-the repo, producing dead ``https://docs.fastpix.com/...`` URLs. The docs team
-then restructured the doc site — the working URLs now live under
-``https://fastpix.com/docs//`` in ``fixed.yaml``.
-
-Strategy (text-link pairing, not URL pairing):
-
-* Build an anchor-text → new-URL index from ``fixed.yaml`` (authoritative).
- We accept both ``TEXT`` and markdown ``[TEXT](URL)``.
-* Walk every source file (.md, .py, .yaml — excluding the two spec files
- themselves and generated harness reports). For each stale link found
- (anchor or markdown form), look up its TEXT in the fixed.yaml index and
- substitute the stale URL with the new URL.
-* Also build the bare URL→URL substitution from old-yaml↔fixed.yaml pairings
- (covers cases where the URL appears free-standing, with no link wrapper).
-
-Python docstrings escape quotes (``href=\\\"URL\\\"``); we accept both escaped
-and unescaped forms.
-
-URLs that have no anchor-text match in ``fixed.yaml`` are reported and left
-alone — those are out of scope for this migration (per user instruction).
-
-Run: ``python -m tests.migrate_doc_links`` (writes changes)
- ``python -m tests.migrate_doc_links --check`` (dry-run, non-zero exit if changes pending)
-"""
-
-from __future__ import annotations
-
-import re
-import sys
-from collections import defaultdict
-from pathlib import Path
-from typing import Dict, Iterable, List, Set, Tuple
-
-REPO_ROOT = Path(__file__).resolve().parent.parent
-OLD_YAML = REPO_ROOT / "old-yaml.yaml"
-NEW_YAML = REPO_ROOT / "fixed.yaml"
-
-EXCLUDE_DIRS = {"node_modules", ".venv", ".venv-tests", "dist", "build", "__pycache__", ".git"}
-EXCLUDE_FILES = {
- "tests/BROKEN_LINKS_REPORT.md",
- "tests/GET_ENDPOINTS_VALIDATION_REPORT.md",
- "tests/NON_GET_ENDPOINTS_VALIDATION_REPORT.md",
- "old-yaml.yaml",
- "fixed.yaml",
- # The script's own URL maps contain stale URLs as dict keys — must not be
- # rewritten by its own walk, or future re-runs would no longer match.
- "tests/migrate_doc_links.py",
-}
-SCAN_EXTENSIONS = (".md", ".py", ".yaml", ".yml")
-
-# Manual mappings for URLs that exist in the Node SDK but not in fixed.yaml.
-# Source of truth: ../node-sdk/README.md (anchor-text matched between repos).
-MANUAL_URL_MAP: Dict[str, str] = {
- "https://docs.fastpix.com/docs/basic-authentication":
- "https://fastpix.com/docs/getting-started/activate-your-account#authentication-format",
- "https://docs.fastpix.com/docs/live-stream-overview":
- "https://fastpix.com/docs/get-started/live-overview",
- "https://docs.fastpix.com/docs/video-data-overview":
- "https://fastpix.com/docs/concepts/what-video-data-do-we-capture",
- "https://docs.fastpix.com/reference/signingkeys-overview":
- "https://fastpix.com/docs/video-security/secure-media-access-with-jwts",
-}
-# URLs that the Node SDK chose to remove rather than redirect. The wrapping
-# `TEXT` is unwrapped to just `TEXT`.
-ANCHOR_REMOVALS: Set[str] = {
- "https://docs.fastpix.com/reference/create-playbackid-of-stream",
-}
-
-STALE_HOST_RE = re.compile(r"https?://docs\.fastpix\.(?:io|com)/[^\s\"'<>()\[\]`\\]+")
-# Accept both real and escaped quotes around URL.
-ANCHOR_RE = re.compile(
- r']*>([^<]+)'
-)
-MARKDOWN_RE = re.compile(
- r'\[([^\]]+)\]\((https?://docs\.fastpix\.(?:io|com)/[^)\s]+)\)'
-)
-
-
-def extract_fixed_anchor_index(yaml_path: Path) -> Dict[str, str]:
- """anchor_text -> new URL, taken from fixed.yaml."""
- text = yaml_path.read_text(encoding="utf-8")
- idx: Dict[str, Set[str]] = defaultdict(set)
- for url, label in ANCHOR_RE.findall(text):
- idx[label.strip()].add(url)
- for label, url in re.findall(
- r'\[([^\]]+)\]\((https?://(?:www\.)?fastpix\.com/[^)\s]+)\)', text
- ):
- idx[label.strip()].add(url)
- # Only keep unambiguous pairings.
- return {label: next(iter(urls)) for label, urls in idx.items() if len(urls) == 1}
-
-
-def extract_text_to_url(yaml_path: Path) -> Dict[str, Set[str]]:
- text = yaml_path.read_text(encoding="utf-8")
- out: Dict[str, Set[str]] = defaultdict(set)
- for url, label in ANCHOR_RE.findall(text):
- out[label.strip()].add(url)
- for label, url in MARKDOWN_RE.findall(text):
- out[label.strip()].add(url)
- return out
-
-
-def build_yaml_pair_map() -> Dict[str, str]:
- """old_url -> new_url (paired by anchor text across both YAMLs).
-
- Includes the .io→.com host-swap form, '#/' fragment-typo variant, and
- Node-SDK-sourced manual overrides for URLs that don't appear in fixed.yaml.
- """
- old = extract_text_to_url(OLD_YAML)
- new = extract_text_to_url(NEW_YAML)
- url_map: Dict[str, str] = {}
- for label, old_urls in old.items():
- new_urls = new.get(label)
- if not new_urls or len(new_urls) != 1:
- continue
- (new_url,) = tuple(new_urls)
- for old_url in old_urls:
- for variant in _variants(old_url):
- url_map[variant] = new_url
- url_map.update(MANUAL_URL_MAP)
- return url_map
-
-
-def _variants(url: str) -> Set[str]:
- variants = {url, url.replace("docs.fastpix.io", "docs.fastpix.com")}
- if "#" in url:
- head, frag = url.split("#", 1)
- if not frag.startswith("/"):
- variants.add(f"{head}#/{frag}")
- variants.add(f"{head.replace('docs.fastpix.io', 'docs.fastpix.com')}#/{frag}")
- return variants
-
-
-def walk_targets(root: Path) -> Iterable[Path]:
- for ext in SCAN_EXTENSIONS:
- for path in root.rglob(f"*{ext}"):
- rel = path.relative_to(root)
- if set(rel.parts) & EXCLUDE_DIRS:
- continue
- if str(rel) in EXCLUDE_FILES:
- continue
- yield path
-
-
-def rewrite_file(
- text: str,
- yaml_url_map: Dict[str, str],
- fixed_anchor_idx: Dict[str, str],
-) -> Tuple[str, int, List[Tuple[str, str]]]:
- """Return (rewritten_text, num_replacements, unmapped_pairs).
-
- ``unmapped_pairs`` is a list of (stale_url, anchor_text) we couldn't resolve.
- """
- count = 0
-
- # Pass 1: anchor-text replacement (handles SDK docstrings where the stale
- # URL isn't in old-yaml but the anchor text appears in fixed.yaml or in
- # ANCHOR_REMOVALS — for those, unwrap `...` to plain text).
- def _anchor_sub(match: re.Match[str]) -> str:
- nonlocal count
- stale_url = match.group(1)
- anchor_text = match.group(2)
- host = "docs.fastpix.io" in stale_url or "docs.fastpix.com" in stale_url
- if not host:
- return match.group(0)
- if stale_url in ANCHOR_REMOVALS:
- count += 1
- return anchor_text
- new_url = fixed_anchor_idx.get(anchor_text.strip()) or yaml_url_map.get(stale_url)
- if new_url is None:
- return match.group(0)
- count += 1
- return match.group(0).replace(stale_url, new_url)
-
- text = ANCHOR_RE.sub(_anchor_sub, text)
-
- # Pass 2: markdown-link replacement.
- def _md_sub(match: re.Match[str]) -> str:
- nonlocal count
- anchor_text = match.group(1).strip()
- stale_url = match.group(2)
- new_url = fixed_anchor_idx.get(anchor_text) or yaml_url_map.get(stale_url)
- if new_url is None:
- return match.group(0)
- count += 1
- return f"[{match.group(1)}]({new_url})"
-
- text = MARKDOWN_RE.sub(_md_sub, text)
-
- # Pass 3: bare URL substitution from the old↔new YAML pair map.
- for old in sorted(yaml_url_map.keys(), key=len, reverse=True):
- if old in text:
- n = text.count(old)
- text = text.replace(old, yaml_url_map[old])
- count += n
-
- # Collect any leftover stale URLs in this file (for reporting).
- unmapped: List[Tuple[str, str]] = []
- for stale in STALE_HOST_RE.findall(text):
- unmapped.append((stale, ""))
- return text, count, unmapped
-
-
-def _process_files(
- yaml_url_map: Dict[str, str],
- fixed_anchor_idx: Dict[str, str],
- check_only: bool,
-) -> Tuple[List[Tuple[Path, int]], Dict[str, Set[str]]]:
- changes: List[Tuple[Path, int]] = []
- unmapped_global: Dict[str, Set[str]] = defaultdict(set)
-
- for path in walk_targets(REPO_ROOT):
- try:
- original = path.read_text(encoding="utf-8")
- except (UnicodeDecodeError, OSError):
- continue
- rewritten, n, unmapped = rewrite_file(original, yaml_url_map, fixed_anchor_idx)
- if n:
- changes.append((path, n))
- if not check_only:
- path.write_text(rewritten, encoding="utf-8")
- for url, _label in unmapped:
- unmapped_global[url].add(str(path.relative_to(REPO_ROOT)))
-
- return changes, unmapped_global
-
-
-def _report_changes(changes: List[Tuple[Path, int]], check_only: bool) -> None:
- if not changes:
- print("\nNo stale doc URLs needed rewriting.")
- return
- verb = "Would rewrite" if check_only else "Rewrote"
- total = sum(n for _, n in changes)
- print(f"\n{verb} {total} URL occurrences across {len(changes)} files:")
- for p, n in sorted(changes):
- print(f" {p.relative_to(REPO_ROOT)}: {n}")
-
-
-def _report_unmapped(unmapped_global: Dict[str, Set[str]]) -> None:
- if not unmapped_global:
- return
- print("\nUnmapped stale URLs left in repo (no pair in fixed.yaml — out of scope):")
- for url, files in sorted(unmapped_global.items()):
- print(f" {url}")
- for f in sorted(files):
- print(f" in {f}")
-
-
-def main(argv: List[str]) -> int:
- check_only = "--check" in argv
- yaml_url_map = build_yaml_pair_map()
- fixed_anchor_idx = extract_fixed_anchor_index(NEW_YAML)
- print(f"Loaded {len(yaml_url_map)} URL substitutions from old↔new YAML pairing.")
- print(f"Loaded {len(fixed_anchor_idx)} anchor-text → new-URL entries from fixed.yaml.")
-
- changes, unmapped_global = _process_files(
- yaml_url_map, fixed_anchor_idx, check_only
- )
-
- _report_changes(changes, check_only)
- _report_unmapped(unmapped_global)
- return 1 if check_only and changes else 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main(sys.argv[1:]))
diff --git a/tests/non_get_endpoints_fixtures.json b/tests/non_get_endpoints_fixtures.json
index 11d4353..f524b20 100644
--- a/tests/non_get_endpoints_fixtures.json
+++ b/tests/non_get_endpoints_fixtures.json
@@ -1,5 +1,5 @@
{
- "_help": "Fixtures for validate_non_get_endpoints.py. CREATE entries provide the request body for each create step; the harness captures returned IDs into a shared context and threads them through UPDATE/DELETE steps. UPDATE/DELETE entries here are reference shapes only — at runtime the harness merges them with IDs from context. Set `skip: true` to keep an entry as documentation without executing it. The harness uses snake_case for top-level kwargs; nested dicts use the SDK's TypedDict aliases (camelCase) which pydantic resolves at construction.",
+ "_help": "Fixtures for validate_non_get_endpoints.py. CREATE entries provide the request body for each create step; the harness captures returned IDs into a shared context and threads them through UPDATE/DELETE steps. UPDATE/DELETE entries here are reference shapes only \u2014 at runtime the harness merges them with IDs from context. Set `skip: true` to keep an entry as documentation without executing it. The harness uses snake_case for top-level kwargs; nested dicts use the SDK's TypedDict aliases (camelCase) which pydantic resolves at construction.",
"operations": {
"create-media": {
"description": "Create an on-demand media from a public URL.",
@@ -10,23 +10,25 @@
"url": "https://static.fastpix.com/sample.mp4"
}
],
- "metadata": { "source": "non-get-validator" },
+ "metadata": {
+ "source": "non-get-validator"
+ },
"accessPolicy": "public",
"maxResolution": "720p"
}
},
-
"direct-upload-video-media": {
"description": "Mint a signed URL for a direct browser upload. Non-destructive.",
"request": {
"corsOrigin": "*",
"pushMediaSettings": {
"accessPolicy": "public",
- "metadata": { "source": "non-get-validator" }
+ "metadata": {
+ "source": "non-get-validator"
+ }
}
}
},
-
"create-a-playlist": {
"description": "Create a manual playlist.",
"request": {
@@ -38,25 +40,27 @@
"metadata": {}
}
},
-
"create-new-stream": {
"description": "Create a new live stream. Costs nothing until it goes live.",
"request": {
- "metadata": { "source": "non-get-validator" },
- "playbackSettings": { "accessPolicy": "public" },
+ "metadata": {
+ "source": "non-get-validator"
+ },
+ "playbackSettings": {
+ "accessPolicy": "public"
+ },
"inputMediaSettings": {
"maxResolution": "1080p",
"reconnectWindow": 60,
- "mediaPolicy": "public"
+ "mediaPolicy": "public",
+ "enableRecording": true
}
}
},
-
"create_signing_key": {
"description": "Create a new signing key for JWT minting.",
"request": {}
},
-
"Add-media-track": {
"description": "Attach a subtitle track to the media created earlier.",
"request": {
@@ -67,7 +71,6 @@
"closedCaptions": true
}
},
-
"Generate-subtitle-track": {
"description": "Trigger subtitle generation on an audio track.",
"request": {
@@ -75,17 +78,30 @@
"languageName": "English"
}
},
-
"create-media-playback-id": {
"description": "Create an additional playback ID on the media.",
- "request": { "accessPolicy": "public" }
+ "request": {
+ "accessPolicy": "public"
+ }
},
-
"create-playbackId-of-stream": {
"description": "Create an additional playback ID on the live stream.",
- "request": { "accessPolicy": "public" }
+ "request": {
+ "accessPolicy": "public",
+ "accessRestrictions": {
+ "domains": {
+ "defaultPolicy": "allow",
+ "allow": [],
+ "deny": []
+ },
+ "userAgents": {
+ "defaultPolicy": "allow",
+ "allow": [],
+ "deny": []
+ }
+ }
+ }
},
-
"create-simulcast-of-stream": {
"description": "Create a simulcast destination on the live stream.",
"request": {
@@ -93,12 +109,14 @@
"streamKey": "test-stream-key-non-get-validator"
}
},
-
"updated-media": {
"description": "Update metadata on the created media.",
- "request": { "metadata": { "updatedBy": "non-get-validator" } }
+ "request": {
+ "metadata": {
+ "updatedBy": "non-get-validator"
+ }
+ }
},
-
"update-media-track": {
"description": "Replace the URL/language on the created track.",
"request": {
@@ -106,38 +124,47 @@
"languageName": "English"
}
},
-
"cancel-upload": {
"description": "Cancel the in-progress resumable upload from direct-upload-video-media.",
"request": {}
},
-
"update-media-summary": {
"description": "Toggle summary generation.",
- "request": { "generate": true, "summaryLength": 100 }
+ "request": {
+ "generate": true,
+ "summaryLength": 100
+ }
},
"update-media-chapters": {
"description": "Toggle chapter generation.",
- "request": { "generate": true }
+ "request": {
+ "generate": true
+ }
},
"update-media-named-entities": {
"description": "Toggle named-entity extraction.",
- "request": { "generate": true }
+ "request": {
+ "generate": true
+ }
},
"update-media-moderation": {
"description": "Toggle moderation.",
- "request": { "generate": true }
+ "request": {
+ "generate": true
+ }
},
-
"updated-source-access": {
"description": "Enable source-file access.",
- "request": { "sourceAccess": true }
+ "request": {
+ "sourceAccess": true
+ }
},
"updated-mp4Support": {
"description": "Enable mp4 capped_4k support.",
- "request": { "mp4Support": "capped_4k" }
+ "request": {
+ "mp4Support": "capped_4k"
+ }
},
-
"add-media-to-playlist": {
"description": "Add the created media to the created playlist.",
"request": {}
@@ -148,25 +175,49 @@
},
"update-a-playlist": {
"description": "Rename the created playlist.",
- "request": { "name": "Non-GET Validator Playlist (renamed)" }
+ "request": {
+ "name": "Non-GET Validator Playlist (renamed)"
+ }
},
-
"update-domain-restrictions": {
"description": "Set domain restrictions on the created playback ID.",
"request": {
- "domains": { "defaultPolicy": "allow", "allow": [], "deny": [] }
+ "defaultPolicy": "allow",
+ "allow": [],
+ "deny": []
}
},
"update-user-agent-restrictions": {
"description": "Set user-agent restrictions on the created playback ID.",
"request": {
- "userAgents": { "defaultPolicy": "allow", "allow": [], "deny": [] }
+ "defaultPolicy": "allow",
+ "allow": [],
+ "deny": []
+ }
+ },
+ "update-live-stream-domain-restrictions": {
+ "description": "Set domain restrictions on the created live stream playback ID.",
+ "request": {
+ "defaultPolicy": "allow",
+ "allow": [],
+ "deny": []
+ }
+ },
+ "update-live-stream-user-agent-restrictions": {
+ "description": "Set user-agent restrictions on the created live stream playback ID.",
+ "request": {
+ "defaultPolicy": "allow",
+ "allow": [],
+ "deny": []
}
},
-
"update-live-stream": {
"description": "Update metadata on the created live stream.",
- "request": { "metadata": { "updatedBy": "non-get-validator" } }
+ "request": {
+ "metadata": {
+ "updatedBy": "non-get-validator"
+ }
+ }
},
"enable-live-stream": {
"description": "Enable the created live stream.",
@@ -182,20 +233,47 @@
"expectedFailReason": "No active RTMPS encoder is broadcasting to the stream, so the API rejects 'finish' with 400.",
"request": {}
},
-
"update-specific-simulcast-of-stream": {
"description": "Toggle isEnabled on the created simulcast.",
- "request": { "isEnabled": true }
- },
-
- "delete-media-track": { "description": "Delete the created track.", "request": {} },
- "delete-media-playback-id": { "description": "Delete the created media playback ID.", "request": {} },
- "delete-media-from-playlist": { "description": "Remove the created media from the playlist.", "request": {} },
- "delete-playbackId-of-stream": { "description": "Delete the created stream playback ID.", "request": {} },
- "delete-simulcast-of-stream": { "description": "Delete the created simulcast.", "request": {} },
- "delete-media": { "description": "Delete the created media.", "request": {} },
- "delete-a-playlist": { "description": "Delete the created playlist.", "request": {} },
- "delete-live-stream": { "description": "Delete the created live stream.", "request": {} },
- "delete_signing_key": { "description": "Delete the created signing key.", "request": {} }
+ "request": {
+ "isEnabled": true
+ }
+ },
+ "delete-media-track": {
+ "description": "Delete the created track.",
+ "request": {}
+ },
+ "delete-media-playback-id": {
+ "description": "Delete the created media playback ID.",
+ "request": {}
+ },
+ "delete-media-from-playlist": {
+ "description": "Remove the created media from the playlist.",
+ "request": {}
+ },
+ "delete-playbackId-of-stream": {
+ "description": "Delete the created stream playback ID.",
+ "request": {}
+ },
+ "delete-simulcast-of-stream": {
+ "description": "Delete the created simulcast.",
+ "request": {}
+ },
+ "delete-media": {
+ "description": "Delete the created media.",
+ "request": {}
+ },
+ "delete-a-playlist": {
+ "description": "Delete the created playlist.",
+ "request": {}
+ },
+ "delete-live-stream": {
+ "description": "Delete the created live stream.",
+ "request": {}
+ },
+ "delete_signing_key": {
+ "description": "Delete the created signing key.",
+ "request": {}
+ }
}
}
diff --git a/tests/openapi_validator_sidecar.mjs b/tests/openapi_validator_sidecar.mjs
index 3efc1db..fa06796 100644
--- a/tests/openapi_validator_sidecar.mjs
+++ b/tests/openapi_validator_sidecar.mjs
@@ -2,7 +2,7 @@
/**
* Long-lived OpenAPI response-validation sidecar.
*
- * Loads ``fixed.yaml`` once, then reads JSON-Lines validation requests from
+ * Loads the repo-root ``openapi.yaml`` once, then reads JSON-Lines validation requests from
* stdin and writes JSON-Lines results to stdout — one request, one result.
*
* Request shape:
@@ -36,7 +36,7 @@ const OpenAPIResponseValidator =
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
-const SPEC_PATH = join(__dirname, "..", "fixed.yaml");
+const SPEC_PATH = join(__dirname, "..", "openapi.yaml");
const spec = yaml.load(readFileSync(SPEC_PATH, "utf-8"));
@@ -82,7 +82,7 @@ for (const [_path, item] of Object.entries(spec.paths || {})) {
}
process.stderr.write(
- `[sidecar] loaded ${validatorByOpId.size} operation validators from fixed.yaml\n`,
+ `[sidecar] loaded ${validatorByOpId.size} operation validators from openapi.yaml\n`,
);
// Ready signal — Python waits for this before piping requests.
process.stdout.write(JSON.stringify({ ready: true, operations: validatorByOpId.size }) + "\n");
diff --git a/tests/run_python_sdk.py b/tests/run_python_sdk.py
deleted file mode 100644
index 0f75436..0000000
--- a/tests/run_python_sdk.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import json, os, sys, traceback
-
-def to_jsonable(x):
- if hasattr(x, "model_dump"):
- try:
- return x.model_dump(by_alias=True)
- except Exception:
- pass
- if hasattr(x, "dict"):
- try:
- return x.dict()
- except Exception:
- pass
- return x
-
-def headers_to_obj(h):
- try:
- return dict(h)
- except Exception:
- pass
- try:
- return dict(h.items())
- except Exception:
- return None
-
-def normalize_err(e):
- out = {
- "name": e.__class__.__name__,
- "message": str(e),
- "stack": traceback.format_exc(),
- }
- status_code = getattr(e, "status_code", None)
- if status_code is not None:
- out["statusCode"] = status_code
- body = getattr(e, "body", None)
- if body is not None:
- out["body"] = body
- if isinstance(body, str):
- try:
- out["bodyJson"] = json.loads(body)
- except Exception:
- pass
- raw = getattr(e, "raw_response", None)
- if raw is not None:
- try:
- out["contentType"] = raw.headers.get("content-type")
- except Exception:
- pass
- try:
- out["headers"] = headers_to_obj(raw.headers)
- except Exception:
- pass
- try:
- out["url"] = str(raw.url)
- except Exception:
- pass
- if getattr(e, "__cause__", None) is not None:
- out["cause"] = str(getattr(e, "__cause__"))
- return out
-
-payload = json.load(sys.stdin)
-op = payload.get("operationId")
-req = payload.get("request") or {}
-base_url = payload.get("baseUrl")
-username = payload.get("username")
-password = payload.get("password")
-
-try:
- from fastpix_python import Fastpix, models
-except Exception as e:
- print(json.dumps({"ok": False, "error": {"name": "PythonImportError", "message": str(e), "stack": traceback.format_exc()}}))
- sys.exit(0)
-
-sdk = Fastpix(security=models.Security(username=username, password=password), server_url=base_url)
-
-def g(k): return req.get(k)
-
-try:
- if op == "list-media":
- res = sdk.manage_videos.list_media(limit=g("limit"), offset=g("offset"), order_by=g("orderBy"))
- elif op == "get-media":
- res = sdk.media.get(media_id=g("mediaId"))
- elif op == "get-media-summary":
- res = sdk.manage_videos.get_summary(media_id=g("mediaId"))
- elif op == "retrieveMediaInputInfo":
- res = sdk.media.get_input_info(media_id=g("mediaId"))
- elif op == "list-uploads":
- res = sdk.manage_videos.list_unused_upload_urls(limit=g("limit"), offset=g("offset"), order_by=g("orderBy"))
- elif op == "get-media-clips":
- res = sdk.manage_videos.get_clips(media_id=g("mediaId"))
- elif op == "list-live-clips":
- res = sdk.media.list_live_clips(livestream_id=g("livestreamId"))
- elif op == "get-all-playlists":
- res = sdk.playlists.get_all(limit=g("limit"), offset=g("offset"))
- elif op == "get-playlist-by-id":
- res = sdk.playlist.get(playlist_id=g("playlistId"))
- elif op == "list-playback-ids":
- res = sdk.playback.list_playback_ids(media_id=g("mediaId"))
- elif op == "get-playback-id":
- res = sdk.playback.get_by_id(media_id=g("mediaId"), playback_id=g("playbackId"))
- elif op == "getDrmConfiguration":
- res = sdk.drm_configurations.get(limit=g("limit"), offset=g("offset"))
- elif op == "getDrmConfigurationById":
- res = sdk.drm_configurations.get_by_id(drm_configuration_id=g("drmConfigurationId"))
- elif op == "get-all-streams":
- res = sdk.live_streams.list(limit=g("limit"), offset=g("offset"), order_by=g("orderBy"))
- elif op == "get-live-stream-by-id":
- res = sdk.manage_live_stream.get(stream_id=g("streamId"))
- elif op == "get-live-stream-viewer-count-by-id":
- res = sdk.manage_live_stream.get_viewer_count(stream_id=g("streamId"))
- elif op == "get-live-stream-playback-id":
- res = sdk.live_playback.get_playback_id_details(stream_id=g("streamId"), playback_id=g("playbackId"))
- elif op == "get-specific-simulcast-of-stream":
- res = sdk.simulcast_stream.get_simulcast(stream_id=g("streamId"), simulcast_id=g("simulcastId"))
- elif op == "list_signing_keys":
- res = sdk.signing_keys.list_signing_keys(limit=g("limit"), offset=g("offset"))
- elif op == "get-signing_key_by_id":
- res = sdk.signing_keys.get_signing_key_by_id(signing_key_id=g("signingKeyId"))
- elif op == "list_video_views":
- res = sdk.views.list_video_views(timespan=g("timespan"), limit=g("limit"), offset=g("offset"))
- elif op == "get_video_view_details":
- res = sdk.views.get_video_view_details(view_id=g("viewId"))
- elif op == "list_by_top_content":
- res = sdk.views.list_by_top_content(timespan=g("timespan"), limit=g("limit"))
- elif op == "list_dimensions":
- res = sdk.dimensions.list()
- elif op == "list_filter_values_for_dimension":
- res = sdk.dimensions.list_filter_values(dimensions_id=g("dimensionsId"))
- elif op == "list_breakdown_values":
- res = sdk.metrics.list_breakdown_values(metric_id=g("metricId"), timespan=g("timespan"), group_by=g("groupBy"))
- elif op == "list_overall_values":
- res = sdk.metrics.list_overall_values(metric_id=g("metricId"), timespan=g("timespan"))
- elif op == "get_timeseries_data":
- res = sdk.metrics.get_timeseries_data(metric_id=g("metricId"), timespan=g("timespan"), group_by=g("groupBy"))
- elif op == "list_comparison_values":
- res = sdk.metrics.list_comparison_values(timespan=g("timespan"), dimension=g("dimension"), value=g("value"))
- elif op == "list_errors":
- res = sdk.errors.list(timespan=g("timespan"), limit=g("limit"))
- else:
- print(json.dumps({"ok": False, "error": {"name": "SDKMappingError", "message": "No Python SDK method mapping for this operationId"}}))
- sys.exit(0)
-
- print(json.dumps({"ok": True, "value": to_jsonable(res)}, default=str))
-except Exception as e:
- print(json.dumps({"ok": False, "error": normalize_err(e)}, default=str))
diff --git a/tests/shims.d.ts b/tests/shims.d.ts
deleted file mode 100644
index d30b744..0000000
--- a/tests/shims.d.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-// Minimal shims so this repo can typecheck the validator script without requiring node_modules installs.
-
-// ESM `import.meta.url`
-interface ImportMeta {
- url: string;
-}
-
-declare module "node:fs" {
- export const readFileSync: any;
- export const writeFileSync: any;
- export const existsSync: any;
- export const mkdirSync: any;
-}
-
-declare module "node:path" {
- export const join: any;
- export const dirname: any;
-}
-
-declare module "node:url" {
- export const fileURLToPath: any;
-}
-
-declare module "node:module" {
- export const createRequire: any;
-}
-
-declare module "node:child_process" {
- export const spawnSync: any;
-}
-
-declare module "js-yaml" {
- const yaml: any;
- export default yaml;
-}
-
-declare const process: any;
-declare const Buffer: any;
-
diff --git a/tests/test_async_errors.py b/tests/test_async_errors.py
new file mode 100644
index 0000000..91c2fc1
--- /dev/null
+++ b/tests/test_async_errors.py
@@ -0,0 +1,41 @@
+"""Async error-path tests: failed responses must raise, never return None."""
+
+import httpx
+import pytest
+
+from fastpix_python import Fastpix, errors, models
+
+
+def _sdk(status: int, body: str):
+ def handler(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(status, text=body, headers={"content-type": "application/json"})
+
+ return Fastpix(
+ security=models.Security(username="u", password="p"),
+ async_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
+ client=httpx.Client(transport=httpx.MockTransport(handler)),
+ )
+
+
+@pytest.mark.asyncio
+async def test_async_4xx_raises_typed_error():
+ sdk = _sdk(404, '{"success":false,"error":{"code":404,"message":"not found"}}')
+ with pytest.raises(errors.FastpixError):
+ await sdk.live_playback.get_live_stream_playback_id_async(stream_id="s", playback_id="p")
+
+
+@pytest.mark.asyncio
+async def test_async_5xx_raises_default_error():
+ sdk = _sdk(500, "boom")
+ with pytest.raises(errors.FastpixDefaultError):
+ await sdk.live_playback.update_live_stream_domain_restrictions_async(
+ stream_id="s", playback_id="p", default_policy="deny", allow=["example.com"]
+ )
+
+
+def test_sync_4xx_raises_for_parity():
+ sdk = _sdk(403, '{"success":false,"error":{"code":403,"message":"forbidden"}}')
+ with pytest.raises(errors.FastpixError):
+ sdk.live_playback.update_live_stream_user_agent_restrictions(
+ stream_id="s", playback_id="p", deny=["PostmanRuntime/7.29.0"]
+ )
diff --git a/tests/test_models.py b/tests/test_models.py
new file mode 100644
index 0000000..ba4f1f0
--- /dev/null
+++ b/tests/test_models.py
@@ -0,0 +1,219 @@
+"""Model contract tests: pure pydantic round-trips, no network.
+
+Pins the wire-format contracts of the SDK's request/response models — field
+types, aliases, defaults, optionality, and serialized body shapes."""
+
+import json
+
+import pytest
+from pydantic import ValidationError
+
+from fastpix_python import models
+from fastpix_python.models.createlivestreamrequest import (
+ CreateLiveStreamRequest,
+ InputMediaSettings,
+)
+from fastpix_python.models.media import Media
+from fastpix_python.models.mediaclipresponse import MediaClipResponseData
+from fastpix_python.models.playbackidrequest import PlaybackIDRequest
+from fastpix_python.models.playbackidresponse import PlaybackIDResponse
+from fastpix_python.models.playbackidsuccessresponse import PlaybackIDSuccessResponse
+from fastpix_python.models.playbacksettings import PlaybackSettings
+from fastpix_python.models.playlistbyidresponse import PlaylistByIDResponseMediaList
+from fastpix_python.models.playlistcreatedschema import PlaylistCreatedSchemaMediaList
+
+RESTRICTIONS = {
+ "domains": {"defaultPolicy": "deny", "allow": ["example.com"], "deny": []},
+ "userAgents": {"defaultPolicy": "allow", "allow": [], "deny": ["PostmanRuntime/7.29.0"]},
+}
+
+
+# ---------------------------------------------------------------------------
+# media duration: float seconds, optional
+# ---------------------------------------------------------------------------
+
+DURATION_MODELS = [
+ Media,
+ MediaClipResponseData,
+ PlaylistCreatedSchemaMediaList,
+ PlaylistByIDResponseMediaList,
+]
+
+
+@pytest.mark.parametrize("cls", DURATION_MODELS)
+def test_duration_accepts_float(cls):
+ assert cls.model_validate({"duration": 145.821315}).duration == 145.821315
+
+
+@pytest.mark.parametrize("cls", DURATION_MODELS)
+def test_duration_accepts_int(cls):
+ assert cls.model_validate({"duration": 10}).duration == 10.0
+
+
+@pytest.mark.parametrize("cls", DURATION_MODELS)
+def test_duration_optional(cls):
+ assert cls.model_validate({}).duration is None
+
+
+@pytest.mark.parametrize("cls", DURATION_MODELS)
+def test_duration_rejects_timestamp_string(cls):
+ with pytest.raises(ValidationError, match="duration"):
+ cls.model_validate({"duration": "00:02:25"})
+
+
+def test_media_duration_serializes_numeric():
+ dumped = Media.model_validate({"duration": 10.5}).model_dump(
+ mode="json", by_alias=True, exclude_unset=True
+ )
+ assert dumped["duration"] == 10.5
+
+
+# ---------------------------------------------------------------------------
+# enableRecording on create live stream
+# ---------------------------------------------------------------------------
+
+
+def test_enable_recording_defaults_true():
+ assert InputMediaSettings().enable_recording is True
+
+
+def test_enable_recording_alias_round_trip():
+ ims = InputMediaSettings.model_validate({"enableRecording": False})
+ assert ims.enable_recording is False
+ wire = json.loads(ims.model_dump_json(by_alias=True, exclude_none=True))
+ assert wire["enableRecording"] is False
+
+
+def test_create_live_stream_request_carries_enable_recording():
+ req = CreateLiveStreamRequest.model_validate(
+ {
+ "playbackSettings": {"accessPolicy": "public"},
+ "inputMediaSettings": {"metadata": {"k": "v"}, "enableRecording": False},
+ }
+ )
+ wire = json.loads(req.model_dump_json(by_alias=True, exclude_none=True))
+ assert wire["inputMediaSettings"]["enableRecording"] is False
+
+
+# ---------------------------------------------------------------------------
+# accessRestrictions on playback models
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize("cls", [PlaybackIDRequest, PlaybackSettings, PlaybackIDResponse])
+def test_access_restrictions_parse(cls):
+ obj = cls.model_validate({"accessPolicy": "public", "accessRestrictions": RESTRICTIONS})
+ ar = obj.access_restrictions
+ assert ar.domains.default_policy == "deny"
+ assert ar.domains.allow == ["example.com"]
+ assert ar.user_agents.default_policy == "allow"
+ assert ar.user_agents.deny == ["PostmanRuntime/7.29.0"]
+
+
+@pytest.mark.parametrize("cls", [PlaybackIDRequest, PlaybackSettings])
+def test_access_restrictions_serialize_wire_aliases(cls):
+ obj = cls.model_validate({"accessPolicy": "public", "accessRestrictions": RESTRICTIONS})
+ wire = json.loads(obj.model_dump_json(by_alias=True, exclude_none=True))
+ assert wire["accessRestrictions"] == RESTRICTIONS
+
+
+@pytest.mark.parametrize("cls", [PlaybackIDRequest, PlaybackSettings])
+def test_access_restrictions_optional(cls):
+ assert cls.model_validate({"accessPolicy": "public"}).access_restrictions is None
+
+
+def test_playback_id_success_response_with_restrictions():
+ resp = PlaybackIDSuccessResponse.model_validate(
+ {
+ "success": True,
+ "data": {
+ "id": "8863f89a-eb6c-4729-8d38-594c9dc25ade",
+ "accessPolicy": "public",
+ "accessRestrictions": RESTRICTIONS,
+ },
+ }
+ )
+ assert resp.data.access_restrictions.domains.allow == ["example.com"]
+
+
+def test_playback_id_success_response_without_restrictions():
+ resp = PlaybackIDSuccessResponse.model_validate(
+ {"success": True, "data": {"id": "x", "accessPolicy": "public"}}
+ )
+ assert resp.data.access_restrictions is None
+
+
+# ---------------------------------------------------------------------------
+# live stream restriction endpoint models
+# ---------------------------------------------------------------------------
+
+LIVE_OPS = [
+ (
+ models.UpdateLiveStreamDomainRestrictionsRequest,
+ models.UpdateLiveStreamDomainRestrictionsRequestBody,
+ models.UpdateLiveStreamDomainRestrictionsResponseBody,
+ ),
+ (
+ models.UpdateLiveStreamUserAgentRestrictionsRequest,
+ models.UpdateLiveStreamUserAgentRestrictionsRequestBody,
+ models.UpdateLiveStreamUserAgentRestrictionsResponseBody,
+ ),
+]
+
+
+@pytest.mark.parametrize("request_cls,body_cls,response_cls", LIVE_OPS)
+def test_live_restriction_request_body_serializes_flat(request_cls, body_cls, response_cls):
+ body = body_cls(default_policy="deny", allow=["example.com"], deny=[])
+ wire = body.model_dump(mode="json", by_alias=True)
+ assert wire == {"defaultPolicy": "deny", "allow": ["example.com"], "deny": []}
+
+
+@pytest.mark.parametrize("request_cls,body_cls,response_cls", LIVE_OPS)
+def test_live_restriction_body_default_policy(request_cls, body_cls, response_cls):
+ assert body_cls().default_policy == "allow"
+
+
+@pytest.mark.parametrize("request_cls,body_cls,response_cls", LIVE_OPS)
+def test_live_restriction_body_omits_unset_lists(request_cls, body_cls, response_cls):
+ wire = body_cls(default_policy="allow").model_dump(mode="json", by_alias=True)
+ assert wire == {"defaultPolicy": "allow"}
+
+
+@pytest.mark.parametrize("request_cls,body_cls,response_cls", LIVE_OPS)
+def test_live_restriction_request_uses_stream_id(request_cls, body_cls, response_cls):
+ req = request_cls(stream_id="s1", playback_id="p1", body=body_cls())
+ assert req.stream_id == "s1"
+ assert req.playback_id == "p1"
+ assert not hasattr(req, "media_id")
+
+
+@pytest.mark.parametrize("request_cls,body_cls,response_cls", LIVE_OPS)
+def test_live_restriction_response_parses(request_cls, body_cls, response_cls):
+ resp = response_cls.model_validate(
+ {
+ "success": True,
+ "data": {"defaultPolicy": "allow", "allow": ["yourdomain.com"], "deny": []},
+ }
+ )
+ assert resp.success is True
+ assert resp.data.allow == ["yourdomain.com"]
+
+
+# ---------------------------------------------------------------------------
+# SDK surface: methods exist with sync/async pairs
+# ---------------------------------------------------------------------------
+
+
+def test_sdk_methods_exist():
+ from fastpix_python.live_playback import LivePlayback
+ from fastpix_python.playback import Playback
+
+ for name in (
+ "update_live_stream_domain_restrictions",
+ "update_live_stream_domain_restrictions_async",
+ "update_live_stream_user_agent_restrictions",
+ "update_live_stream_user_agent_restrictions_async",
+ ):
+ assert hasattr(LivePlayback, name), name
+ for name in ("update_domain_restrictions_async", "update_user_agent_restrictions_async"):
+ assert hasattr(Playback, name), name
diff --git a/tests/test_return_annotations.py b/tests/test_return_annotations.py
new file mode 100644
index 0000000..5c06135
--- /dev/null
+++ b/tests/test_return_annotations.py
@@ -0,0 +1,57 @@
+"""Every resource method's declared return type must be the class it actually
+unmarshals on the success path. Guards against annotations drifting from the
+`{success, data}` envelope the API returns."""
+
+import ast
+import pathlib
+
+import pytest
+
+PKG = pathlib.Path(__file__).resolve().parent.parent / "fastpix_python"
+SKIP = {"basesdk.py", "sdk.py", "httpclient.py", "sdkconfiguration.py", "_version.py", "__init__.py"}
+
+
+def _success_unmarshal_class(fn: ast.FunctionDef) -> str | None:
+ """Return the models.X class passed to unmarshal_json_response inside the
+ `if utils.match_response(http_res, "2xx", ...)` branch, if any."""
+ for node in ast.walk(fn):
+ if not isinstance(node, ast.If):
+ continue
+ test = node.test
+ if not (isinstance(test, ast.Call) and getattr(test.func, "attr", "") == "match_response"):
+ continue
+ status = test.args[1] if len(test.args) > 1 else None
+ if not (isinstance(status, ast.Constant) and str(status.value).startswith("2")):
+ continue
+ for inner in ast.walk(node):
+ if isinstance(inner, ast.Return) and isinstance(inner.value, ast.Call):
+ call = inner.value
+ if getattr(call.func, "id", "") == "unmarshal_json_response" and call.args:
+ return ast.unparse(call.args[0])
+ return None
+
+
+def _cases():
+ for path in sorted(PKG.glob("*.py")):
+ if path.name in SKIP:
+ continue
+ tree = ast.parse(path.read_text())
+ for cls in (n for n in tree.body if isinstance(n, ast.ClassDef)):
+ for fn in (n for n in cls.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))):
+ if fn.name.startswith("_") or fn.returns is None:
+ continue
+ actual = _success_unmarshal_class(fn)
+ if actual:
+ yield pytest.param(path.name, fn.name, ast.unparse(fn.returns), actual, id=f"{path.stem}.{fn.name}")
+
+
+CASES = list(_cases())
+
+
+def test_scan_found_methods():
+ assert len(CASES) > 100
+
+
+@pytest.mark.parametrize("module,method,declared,actual", CASES)
+def test_return_annotation_matches_unmarshaled_class(module, method, declared, actual):
+ assert declared == actual, f"{module}::{method} declares {declared} but unmarshals {actual}"
diff --git a/tests/tsconfig.json b/tests/tsconfig.json
deleted file mode 100644
index 5752ecb..0000000
--- a/tests/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "lib": ["ES2022", "DOM"],
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "strict": true,
- "skipLibCheck": true
- },
- "include": ["./validate-get-endpoints.ts", "./shims.d.ts"]
-}
-
diff --git a/tests/validate-get-endpoints.ts b/tests/validate-get-endpoints.ts
deleted file mode 100644
index 789a714..0000000
--- a/tests/validate-get-endpoints.ts
+++ /dev/null
@@ -1,1183 +0,0 @@
-#!/usr/bin/env tsx
-/*
- * GET endpoints validator using `openapi-response-validator`
- *
- * Per GET endpoint in `fixed.yaml`:
- * - Calls the API to get the raw JSON response
- * - Validates the raw response against the OpenAPI response schema using `openapi-response-validator`
- * - Parses the same raw response through the SDK's Zod inbound schema (this is what the SDK returns)
- * - Compares JSON paths:
- * - missingInSDK: present in API raw JSON but missing after SDK parsing
- * - missingInAPI: present after SDK parsing but missing in API raw JSON
- * - Generates a consolidated markdown report.
- *
- * Requirements:
- * - FASTPIX_USERNAME / FASTPIX_PASSWORD env vars (Basic Auth)
- * - `tests/get-endpoints-fixtures.json` for endpoints with required path params (optional but recommended)
- */
-
-///
-
-import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
-import { spawnSync } from "node:child_process";
-import { join, dirname } from "node:path";
-import { fileURLToPath } from "node:url";
-import { createRequire } from "node:module";
-import yaml from "js-yaml";
-
-const require = createRequire(import.meta.url);
-const openapiResponseValidatorMod = require("openapi-response-validator");
-const OpenAPIResponseValidator =
- openapiResponseValidatorMod?.default ?? openapiResponseValidatorMod;
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-type Fixture = {
- operations: Record<
- string,
- {
- pathParams?: Record;
- query?: Record>;
- }
- >;
-};
-
-type EndpointInfo = {
- path: string;
- method: "GET";
- operationId: string;
- responses: any;
- parameters: Array;
-};
-
-type FixSuggestion = {
- title: string;
- why: string;
- where?: string;
- pasteYaml?: string;
-};
-
-type EndpointResult = {
- endpoint: string;
- operationId: string;
- method: "GET";
- openapiValid: boolean;
- openapiErrors: Array<{ path?: string; message?: string; errorCode?: string }>;
- sdkParseOk: boolean;
- sdkParseError?: string;
- missingInSDK: string[];
- missingInAPI: string[];
- emptyArraysOmittedInSDK: string[];
- emptyArraysOmittedInAPI: string[];
- apiResponseFile?: string;
- sdkResponseFile?: string;
- apiResponsePreview?: string;
- sdkResponsePreview?: string;
- status: "PASS" | "FAIL";
- note?: string;
- fixSuggestions?: FixSuggestion[];
-};
-
-const ARTIFACTS_DIRNAME = "artifacts";
-const MAX_PREVIEW_CHARS = 4000;
-const PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000";
-const FIX_SUGGESTIONS_MD = "GET_ENDPOINTS_OPENAPI_RESPONSE_FIX_SUGGESTIONS.md";
-
-function safeFileSlug(input: string): string {
- return input.replace(/[^a-zA-Z0-9._-]+/g, "_");
-}
-
-function toPrettyJson(value: unknown): string {
- return JSON.stringify(value, null, 2);
-}
-
-function preview(text: string): string {
- if (text.length <= MAX_PREVIEW_CHARS) return text;
- return text.slice(0, MAX_PREVIEW_CHARS) + "\n... (truncated)";
-}
-
-function writeArtifactFiles(
- operationId: string,
- rawBody: unknown,
- sdkBody: unknown,
-): {
- apiPath: string;
- sdkPath: string;
- apiPreview: string;
- sdkPreview: string;
-} {
- const artifactsDir = join(__dirname, ARTIFACTS_DIRNAME);
- mkdirSync(artifactsDir, { recursive: true });
-
- const slug = safeFileSlug(operationId);
- const apiFilename = `${slug}.api.json`;
- const sdkFilename = `${slug}.sdk.json`;
-
- const apiText = toPrettyJson(rawBody);
- const sdkText = toPrettyJson(sdkBody);
-
- const apiPath = join(artifactsDir, apiFilename);
- const sdkPath = join(artifactsDir, sdkFilename);
-
- writeFileSync(apiPath, apiText);
- writeFileSync(sdkPath, sdkText);
-
- return {
- apiPath: `tests/${ARTIFACTS_DIRNAME}/${apiFilename}`,
- sdkPath: `tests/${ARTIFACTS_DIRNAME}/${sdkFilename}`,
- apiPreview: preview(apiText),
- sdkPreview: preview(sdkText),
- };
-}
-
-function defaultSDKRequest(operationId: string): any {
- // Ensure SDK input validation passes so we reach the HTTP call and get server errors on failures.
- switch (operationId) {
- case "get-media":
- case "get-media-summary":
- case "retrieveMediaInputInfo":
- case "list-playback-ids":
- case "get-media-clips":
- return { mediaId: PLACEHOLDER_UUID };
- case "get-playback-id":
- return { mediaId: PLACEHOLDER_UUID, playbackId: PLACEHOLDER_UUID };
- case "list-live-clips":
- return { livestreamId: PLACEHOLDER_UUID };
- case "get-playlist-by-id":
- return { playlistId: PLACEHOLDER_UUID };
- case "getDrmConfigurationById":
- return { drmConfigurationId: PLACEHOLDER_UUID };
- case "get-live-stream-by-id":
- case "get-live-stream-viewer-count-by-id":
- return { streamId: PLACEHOLDER_UUID };
- case "get-live-stream-playback-id":
- return { streamId: PLACEHOLDER_UUID, playbackId: PLACEHOLDER_UUID };
- case "get-specific-simulcast-of-stream":
- return { streamId: PLACEHOLDER_UUID, simulcastId: PLACEHOLDER_UUID };
- case "get-signing_key_by_id":
- return { signingKeyId: PLACEHOLDER_UUID };
- case "get_video_view_details":
- return { viewId: PLACEHOLDER_UUID };
- case "list_filter_values_for_dimension":
- return { dimensionsId: "browser_name" };
- case "list_breakdown_values":
- return {
- metricId: "quality_of_experience_score",
- timespan: "24:hours",
- groupBy: "browser_name",
- };
- case "list_overall_values":
- return { metricId: "quality_of_experience_score", timespan: "24:hours" };
- case "get_timeseries_data":
- return {
- metricId: "quality_of_experience_score",
- timespan: "24:hours",
- groupBy: "hour",
- };
- case "list_comparison_values":
- return { timespan: "24:hours", dimension: "browser_name", value: "Chrome" };
- case "list_errors":
- return { timespan: "24:hours", limit: 5 };
- case "list_video_views":
- return { timespan: "24:hours", limit: 5, offset: 1 };
- case "list_by_top_content":
- return { timespan: "24:hours", limit: 5 };
- case "list-media":
- return { limit: 5, offset: 1, orderBy: "desc" };
- case "list-uploads":
- return { limit: 5, offset: 1, orderBy: "desc" };
- case "get-all-streams":
- return { limit: 5, offset: 1, orderBy: "desc" };
- case "getDrmConfiguration":
- return { limit: 10, offset: 1 };
- case "get-all-playlists":
- return { limit: 5, offset: 1 };
- case "list_signing_keys":
- return { limit: 5, offset: 1 };
- case "list_dimensions":
- return undefined;
- default:
- return undefined;
- }
-}
-
-function buildSDKRequest(endpoint: EndpointInfo, fixtures: Fixture | null): any {
- const opFixture = fixtures?.operations?.[endpoint.operationId];
- const fromFixture = opFixture
- ? { ...opFixture.pathParams, ...opFixture.query }
- : undefined;
-
- // If fixtures exist, use them as-is (they match SDK request shapes).
- if (fromFixture) return fromFixture;
-
- // Prefer operation-specific defaults (handles required query params too).
- const def = defaultSDKRequest(endpoint.operationId);
- if (def !== undefined) return def;
-
- // Otherwise: auto-generate a placeholder request object for required path params.
- const requiredPathParams = endpoint.parameters
- .filter((p) => p?.in === "path" && p?.required)
- .map((p) => p.name);
-
- if (requiredPathParams.length === 0) return undefined;
-
- const req: Record = {};
- for (const name of requiredPathParams) req[name] = PLACEHOLDER_UUID;
- return req;
-}
-
-function headersToObject(headers: any): Record | undefined {
- try {
- if (!headers) return undefined;
- if (typeof headers.entries === "function") {
- return Object.fromEntries(Array.from(headers.entries()));
- }
- } catch {
- // ignore
- }
- return undefined;
-}
-
-function normalizeSdkError(err: any): any {
- const base: any = {
- name: err?.name,
- message: err?.message,
- stack: err?.stack,
- };
-
- if (err?.statusCode !== undefined) base.statusCode = err.statusCode;
- if (err?.contentType !== undefined) base.contentType = err.contentType;
- if (err?.body !== undefined) {
- base.body = err.body;
- if (typeof err.body === "string") {
- try {
- base.bodyJson = JSON.parse(err.body);
- } catch {
- // ignore
- }
- }
- }
- base.headers = headersToObject(err?.headers) ?? headersToObject(err?.rawResponse?.headers);
- if (err?.rawResponse?.url) base.url = err.rawResponse.url;
-
- if (err?.cause) base.cause = err.cause;
- if (err?.rawMessage !== undefined) base.rawMessage = err.rawMessage;
- if (err?.rawValue !== undefined) base.rawValue = err.rawValue;
-
- return base;
-}
-
-type PythonSDKResult =
- | { ok: true; value: any }
- | { ok: false; error: any };
-
-function tryResolvePythonSdkSrc(): string {
- // When running from fastpix-python/tests, the local import root is ../src (src-layout).
- // Keep deterministic fallbacks for workspace layouts.
- const candidates = [
- join(__dirname, "../src"),
- join(__dirname, "../../fastpix-python/src"),
- ];
- for (const p of candidates) {
- if (existsSync(p)) return p;
- }
- throw new Error(`Could not locate fastpix-python/src. Tried: ${candidates.map((c) => JSON.stringify(c)).join(", ")}`);
-}
-
-function invokePythonSDK(
- operationId: string,
- request: any,
- baseUrl: string,
- username: string,
- password: string,
-): PythonSDKResult {
- const pySrc = tryResolvePythonSdkSrc();
-
- const pyScriptPath = join(__dirname, "run_python_sdk.py");
-
- const child = spawnSync("python3", [pyScriptPath], {
- input: JSON.stringify({ operationId, request, baseUrl, username, password }),
- encoding: "utf-8",
- env: {
- ...process.env,
- PYTHONPATH: [pySrc, process.env.PYTHONPATH].filter(Boolean).join(":"),
- },
- maxBuffer: 10 * 1024 * 1024,
- });
-
- if (child.error) {
- return { ok: false, error: { name: "PythonSpawnError", message: child.error.message } };
- }
-
- const stdout = (child.stdout || "").trim();
- try {
- const parsed = JSON.parse(stdout);
- if (parsed?.ok) return { ok: true, value: parsed.value };
- return { ok: false, error: parsed?.error ?? { name: "PythonSDKError", message: stdout } };
- } catch {
- return { ok: false, error: { name: "PythonOutputParseError", message: stdout } };
- }
-}
-
-function readFixtures(): Fixture | null {
- const p = join(__dirname, "get-endpoints-fixtures.json");
- if (!existsSync(p)) return null;
- return JSON.parse(readFileSync(p, "utf-8")) as Fixture;
-}
-
-function resolveSpecPath(): string {
- // Deterministic search order (mirrors reference repo’s "../../fastpix.yaml" pattern).
- const candidates = [
- join(__dirname, "../fastpix.yaml"),
- join(__dirname, "../../fastpix.yaml"),
- join(__dirname, "../fixed.yaml"),
- join(__dirname, "../../fixed.yaml"),
- join(__dirname, "../fastpix-openapi.yaml"),
- join(__dirname, "../../fastpix-openapi.yaml"),
- ];
- for (const p of candidates) {
- if (existsSync(p)) return p;
- }
- throw new Error(
- `OpenAPI spec not found. Tried: ${candidates.map((c) => JSON.stringify(c)).join(", ")}`,
- );
-}
-
-function loadOpenAPISpec(): any {
- const specPath = resolveSpecPath();
- return yaml.load(readFileSync(specPath, "utf-8"));
-}
-
-function extractGetEndpoints(spec: any): EndpointInfo[] {
- const out: EndpointInfo[] = [];
- for (const [path, methods] of Object.entries(spec.paths || {})) {
- const m = methods as any;
- if (!m.get) continue;
- out.push({
- path,
- method: "GET",
- operationId: m.get.operationId,
- responses: m.get.responses || {},
- parameters: [...(m.get.parameters || []), ...(m.parameters || [])],
- });
- }
- return out;
-}
-
-// Convert OpenAPI 3 schema refs (#/components/schemas/X) to the format used by openapi-response-validator (#/definitions/X)
-function convertRefsToDefinitions(node: any): any {
- if (node == null || typeof node !== "object") return node;
- if (Array.isArray(node)) return node.map(convertRefsToDefinitions);
- const out: any = {};
- for (const [k, v] of Object.entries(node)) {
- if (k === "$ref" && typeof v === "string") {
- out[k] = v.replace("#/components/schemas/", "#/definitions/");
- } else {
- out[k] = convertRefsToDefinitions(v);
- }
- }
- return out;
-}
-
-function makeOpenAPIResponseValidator(spec: any, endpoint: EndpointInfo) {
- const definitions = convertRefsToDefinitions(spec.components?.schemas || {});
- const responses: any = {};
-
- for (const [status, def] of Object.entries(endpoint.responses || {})) {
- const d = def as any;
- const schema = d?.content?.["application/json"]?.schema;
- if (!schema) continue;
- responses[status] = {
- description: d.description || "",
- schema: convertRefsToDefinitions(schema),
- };
- }
-
- if (Object.keys(responses).length === 0) return null;
-
- return new OpenAPIResponseValidator({
- responses,
- definitions,
- });
-}
-
-function hasOpenapiError(r: EndpointResult, includes: string): boolean {
- return (r.openapiErrors || []).some((e) => (e?.message ?? "").includes(includes));
-}
-
-function openapiErrorPaths(r: EndpointResult): string[] {
- return (r.openapiErrors || [])
- .map((e) => e?.path)
- .filter((p): p is string => typeof p === "string" && p.length > 0);
-}
-
-function generateFixSuggestions(r: EndpointResult): FixSuggestion[] {
- const out: FixSuggestion[] = [];
- const paths = openapiErrorPaths(r);
-
- // 1) Generic: oneOf overlap on tracks
- const hasTracksOneOf =
- hasOpenapiError(r, "must match exactly one schema in oneOf") &&
- paths.some((p) => p.includes("tracks"));
- if (hasTracksOneOf) {
- out.push({
- title: "Fix `tracks[].oneOf` overlap by constraining `type` per track schema",
- why:
- "The current track schemas overlap (e.g. `type` is a free string and distinguishing fields are not required), so a single track object can match multiple branches. `oneOf` requires exactly one match.",
- where:
- "In `fixed.yaml`: `components/schemas/{VideoTrack,VideoTrackForGetAll,AudioTrack,SubtitleTrack}.properties.type`",
- pasteYaml: [
- "# Apply these changes inside each schema’s `properties:` block:",
- "",
- "# VideoTrack (and VideoTrackForGetAll)",
- "type:",
- " type: string",
- " enum: [video]",
- " example: video",
- "",
- "# AudioTrack",
- "type:",
- " type: string",
- " enum: [audio]",
- " example: audio",
- "",
- "# SubtitleTrack",
- "type:",
- " type: string",
- " enum: [subtitle]",
- " example: subtitle",
- ].join("\n"),
- });
- }
-
- // 2) Enum mismatch: sourceResolution
- const hasSourceResolutionEnum =
- hasOpenapiError(r, "must be equal to one of the allowed values") &&
- paths.some((p) => p.includes("sourceResolution"));
- if (hasSourceResolutionEnum) {
- out.push({
- title: "Fix `sourceResolution` enum mismatch (API may return values without `p`)",
- why:
- "The API can return values like `\"1080\"` but the spec constrains the enum to `\"1080p\"`-style values.",
- where:
- "In `fixed.yaml`: under the relevant media response schema(s) `sourceResolution:` field definition",
- });
- }
-
- // 3) Redundant oneOf for /data/dimensions
- const hasDimensionsOneOf =
- hasOpenapiError(r, "must match exactly one schema in oneOf") &&
- (r.endpoint === "/data/dimensions" || paths.some((p) => p.includes("dimensions")));
- if (hasDimensionsOneOf) {
- out.push({
- title: "Remove redundant `oneOf` on `/data/dimensions` response schema",
- why:
- "`data` is defined as `oneOf: [array, $ref: Dimensions]` and `Dimensions` itself is also `array`, so valid responses can match multiple branches.",
- where:
- "In `fixed.yaml`: `paths./data/dimensions.get.responses.200.content.application/json.schema.properties.data.oneOf`",
- });
- }
-
- // 4) Overlapping numeric oneOf: integer vs number
- const hasIntegerVsNumber =
- hasOpenapiError(r, "must match exactly one schema in oneOf") &&
- paths.some((p) => p.includes("value"));
- if (hasIntegerVsNumber) {
- out.push({
- title: "Avoid `oneOf: [integer, number]` overlaps (integers are also numbers)",
- why:
- "In JSON Schema, `integer` is a subset of `number`. A value like `0` matches both, causing oneOf validation errors.",
- where:
- "In `fixed.yaml`: metrics schemas that use `oneOf: [integer, number]`",
- });
- }
-
- // 5) Nullable mismatch: fpApiVersion
- const hasFpApiVersionNull =
- hasOpenapiError(r, "must be string") &&
- paths.some((p) => p.includes("fpApiVersion"));
- if (hasFpApiVersionNull) {
- out.push({
- title: "Make `fpApiVersion` nullable in the spec",
- why: "The API can return `null` for fpApiVersion but the schema declares `string` only.",
- where: "In `fixed.yaml`: `components/schemas/Views.properties.fpApiVersion`",
- });
- }
-
- // 6) Placeholder fixture guidance (common 404)
- const placeholderUsed = (r.note || "").includes("Placeholder used");
- const likely404 =
- r.sdkParseOk === false &&
- /404|not found/i.test(r.sdkParseError || "") &&
- placeholderUsed;
- if (likely404) {
- out.push({
- title: "Provide real fixture IDs for this operationId",
- why:
- "A placeholder UUID was used for required path params; the API likely returned 404 because the resource doesn't exist. Add a real ID under `tests/get-endpoints-fixtures.json` for this operationId.",
- });
- }
-
- // 7) Playlist playOrder default / missing
- const playOrderMissing = r.missingInAPI.some((p) => p.includes("playOrder")) ||
- r.missingInSDK.some((p) => p.includes("playOrder"));
- if (playOrderMissing) {
- out.push({
- title: "Ensure `playOrder` is correctly modeled for smart playlists only",
- why:
- "If `playOrder` is present/required only for `type: smart`, the response schemas should reflect that (e.g. discriminator split).",
- where:
- "In `fixed.yaml`: playlist response schemas for create/update/get-by-id",
- });
- }
-
- // 8) simulcastResponses missing
- const hasSimulcastResponses = r.missingInSDK.some((p) => p.includes("simulcastResponses"));
- if (hasSimulcastResponses) {
- out.push({
- title: "Add `simulcastResponses` to the live stream response schema",
- why:
- "The API response includes simulcastResponses but the OpenAPI schema (and generated SDK inbound schema) does not, causing the SDK to drop the field.",
- where:
- "In `fixed.yaml`: live stream response schema(s) for get/list streams",
- });
- }
-
- return out;
-}
-
-function observedOpenapiErrorLines(r: EndpointResult): string[] {
- if (r.openapiValid || (r.openapiErrors?.length ?? 0) === 0) return [];
- const lines: string[] = ["### Observed OpenAPI errors", ""];
- for (const e of r.openapiErrors) {
- const loc = e.path ? `\`${e.path}\`` : "";
- const msg = e.message ?? "";
- lines.push(`- ${loc} ${msg}`.trim());
- }
- lines.push("");
- return lines;
-}
-
-function suggestionDetailLines(suggestions: FixSuggestion[]): string[] {
- if (suggestions.length === 0) {
- return [
- "### Suggested fixes",
- "",
- "- No heuristic suggestions available for this failure yet.",
- "",
- ];
- }
- const lines: string[] = ["### Suggested fixes", ""];
- for (const s of suggestions) {
- lines.push(`- **${s.title}**`, ` - **why**: ${s.why}`);
- if (s.where) lines.push(` - **where**: ${s.where}`);
- if (s.pasteYaml) {
- lines.push(" - **paste**:", "", "```yaml", s.pasteYaml, "```");
- }
- lines.push("");
- }
- return lines;
-}
-
-function fixSuggestionLines(r: EndpointResult): string[] {
- const lines: string[] = [];
- lines.push(
- `## ${r.operationId} (\`${r.endpoint}\`)`,
- "",
- `- **Status**: ${r.status}`,
- `- **OpenAPI valid**: ${r.openapiValid ? "yes" : "no"}`,
- `- **SDK parse**: ${r.sdkParseOk ? "ok" : "failed"}`,
- );
- if (r.apiResponseFile) lines.push(`- **API artifact**: \`${r.apiResponseFile}\``);
- if (r.sdkResponseFile) lines.push(`- **SDK artifact**: \`${r.sdkResponseFile}\``);
- lines.push(
- "",
- ...observedOpenapiErrorLines(r),
- ...suggestionDetailLines(r.fixSuggestions ?? []),
- );
- return lines;
-}
-
-function writeFixSuggestions(results: EndpointResult[]) {
- const failing = results.filter((r) => r.status === "FAIL");
- const outPath = join(__dirname, FIX_SUGGESTIONS_MD);
- const lines: string[] = [];
-
- lines.push(
- "# GET Endpoints — OpenAPI Response Fix Suggestions",
- "",
- `Generated: ${new Date().toISOString()}`,
- "",
- `Total failing endpoints: ${failing.length}`,
- "",
- );
-
- for (const r of failing) lines.push(...fixSuggestionLines(r));
-
- writeFileSync(outPath, lines.join("\n"));
-}
-
-function addAll(target: Set, source: Iterable): void {
- for (const x of source) target.add(x);
-}
-
-function collectEmptyArrayFieldPaths(value: any, prefix = ""): Set {
- const out = new Set();
- if (value === null || typeof value !== "object") return out;
-
- if (Array.isArray(value)) {
- const arrayPrefix = prefix ? `${prefix}[]` : "[]";
- for (const item of value) addAll(out, collectEmptyArrayFieldPaths(item, arrayPrefix));
- return out;
- }
-
- for (const [k, v] of Object.entries(value)) {
- const p = prefix ? `${prefix}.${k}` : k;
- if (Array.isArray(v) && v.length === 0) out.add(p);
- addAll(out, collectEmptyArrayFieldPaths(v, p));
- }
- return out;
-}
-
-function collectObjectJsonPaths(
- value: Record,
- prefix: string,
- opts: { includeEmptyArrays?: boolean },
- includeEmptyArrays: boolean,
- out: Set,
-): void {
- for (const [k, v] of Object.entries(value)) {
- if (!includeEmptyArrays && Array.isArray(v) && v.length === 0) {
- continue;
- }
- const p = prefix ? `${prefix}.${k}` : k;
- out.add(p);
- addAll(out, collectJsonPaths(v, p, opts));
- }
-}
-
-function collectJsonPaths(
- value: any,
- prefix = "",
- opts: { includeEmptyArrays?: boolean } = {},
-): Set {
- const out = new Set();
- const includeEmptyArrays = opts.includeEmptyArrays ?? true;
-
- if (value === null || value === undefined) return out;
- if (typeof value !== "object") {
- if (prefix) out.add(prefix);
- return out;
- }
-
- if (Array.isArray(value)) {
- if (!includeEmptyArrays && value.length === 0) return out;
- const arrayPrefix = prefix ? `${prefix}[]` : "[]";
- out.add(arrayPrefix);
- for (const item of value) addAll(out, collectJsonPaths(item, arrayPrefix, opts));
- return out;
- }
-
- collectObjectJsonPaths(value, prefix, opts, includeEmptyArrays, out);
- return out;
-}
-
-function sortUnique(arr: string[]) {
- return Array.from(new Set(arr)).sort((a, b) => a.localeCompare(b));
-}
-
-function canonicalizeKey(key: string): string {
- // 1) snake_case -> camelCase
- const camel = key.includes("_")
- ? key
- .toLowerCase()
- .replace(/_([a-z0-9])/g, (_, c) => String(c).toUpperCase())
- : key;
-
- // 2) normalize acronyms casing
- return camel.replaceAll("SDK", "Sdk").replaceAll("API", "Api");
-}
-
-function normalizeJsonForComparison(value: any): any {
- if (value === null || value === undefined) return value;
- if (Array.isArray(value)) return value.map(normalizeJsonForComparison);
- if (typeof value !== "object") return value;
- const out: any = {};
- for (const [k, v] of Object.entries(value)) {
- out[canonicalizeKey(k)] = normalizeJsonForComparison(v);
- }
- return out;
-}
-
-function cloneJsonValue(value: any): any {
- // `value` here is already pure JSON (parsed from the Python process output),
- // so structuredClone produces the same deep copy a JSON round-trip would.
- return structuredClone(value);
-}
-
-function applyPathParams(
- path: string,
- requiredPathParams: string[],
- effectiveReq: Record,
-): { path: string; note?: string } {
- let note: string | undefined;
- for (const name of requiredPathParams) {
- const val = effectiveReq[name] ?? PLACEHOLDER_UUID;
- if (effectiveReq[name] == null) {
- note = note ? `${note}; placeholder used for ${name}` : `Placeholder used for ${name}`;
- }
- path = path.replaceAll(`{${name}}`, encodeURIComponent(val));
- }
- return { path, note };
-}
-
-function applyQueryParams(
- url: URL,
- queryParams: Array,
- effectiveReq: Record,
-): void {
- for (const p of queryParams) {
- const name: string = p.name;
- const baseName = name.endsWith("[]") ? name.slice(0, -2) : name;
- const val = effectiveReq[name] ?? effectiveReq[baseName];
- if (val == null) continue;
-
- if (Array.isArray(val)) {
- for (const item of val) url.searchParams.append(name, String(item));
- } else if (name.endsWith("[]")) {
- url.searchParams.append(name, String(val));
- } else {
- url.searchParams.set(name, String(val));
- }
- }
-}
-
-function buildUrl(
- baseUrl: string,
- endpoint: EndpointInfo,
- fixture: Fixture | null,
-): { url: string; note?: string } {
- const opFixture = fixture?.operations?.[endpoint.operationId];
-
- const requiredPathParams = endpoint.parameters
- .filter((p) => p?.in === "path" && p?.required)
- .map((p) => p.name);
-
- const defaults = defaultSDKRequest(endpoint.operationId) ?? {};
- const fromFixture = opFixture
- ? { ...opFixture.pathParams, ...opFixture.query }
- : {};
- const effectiveReq: Record = { ...defaults, ...fromFixture };
-
- const { path, note } = applyPathParams(
- endpoint.path,
- requiredPathParams,
- effectiveReq,
- );
-
- const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
- const url = new URL(path.replace(/^\//, ""), base);
-
- applyQueryParams(
- url,
- endpoint.parameters.filter((p) => p?.in === "query"),
- effectiveReq,
- );
-
- return { url: url.toString(), note };
-}
-
-function basicAuthHeader(username: string, password: string): string {
- const token = Buffer.from(`${username}:${password}`).toString("base64");
- return `Basic ${token}`;
-}
-
-const CONSOLIDATED_TABLE_HEADER = [
- "| Endpoint | OperationId | OpenAPI valid | SDK parse | Missing in SDK (present in API) | Missing in API (present in SDK) | Empty arrays omitted by SDK | Status |",
- "|---|---|---:|---:|---|---|---|---|",
-];
-
-function joinOrNone(items: string[]): string {
- return items.length ? items.map((p) => `\`${p}\``).join(", ") : "None";
-}
-
-function consolidatedRow(r: EndpointResult): string {
- const openapiCol = r.openapiValid ? "✅" : "❌";
- const sdkCol = r.sdkParseOk ? "✅" : "❌";
- const status = r.status === "PASS" ? "✅ PASS" : "❌ FAIL";
- return `| \`${r.endpoint}\` | \`${r.operationId}\` | ${openapiCol} | ${sdkCol} | ${joinOrNone(r.missingInSDK)} | ${joinOrNone(r.missingInAPI)} | ${joinOrNone(r.emptyArraysOmittedInSDK)} | ${status} |`;
-}
-
-function bulletListOrNone(items: string[]): string[] {
- if (items.length === 0) return ["- None"];
- return items.map((p) => `- \`${p}\``);
-}
-
-function previewLines(label: string, previewText?: string): string[] {
- if (!previewText) return [];
- return [`**${label}**`, "", "```json", previewText, "```", ""];
-}
-
-function endpointDetailLines(r: EndpointResult): string[] {
- const lines: string[] = [];
- lines.push(`### ${r.operationId} (\`${r.endpoint}\`)`, "", `- **Status**: ${r.status}`);
- if (r.note) lines.push(`- **Note**: ${r.note}`);
- lines.push(`- **OpenAPI valid**: ${r.openapiValid ? "yes" : "no"}`);
- if (!r.openapiValid && r.openapiErrors.length) {
- lines.push("- **OpenAPI errors**:");
- for (const e of r.openapiErrors) {
- const loc = e.path ? `\`${e.path}\`` : "";
- const msg = e.message ?? "";
- lines.push(` - ${loc} ${msg}`.trim());
- }
- }
- lines.push(`- **SDK parse**: ${r.sdkParseOk ? "ok" : "failed"}`);
- if (!r.sdkParseOk && r.sdkParseError) lines.push(`- **SDK parse error**: ${r.sdkParseError}`);
- if (r.apiResponseFile) lines.push(`- **API response file**: \`${r.apiResponseFile}\``);
- if (r.sdkResponseFile) lines.push(`- **SDK response file**: \`${r.sdkResponseFile}\``);
- lines.push(
- "",
- ...previewLines("API response (preview)", r.apiResponsePreview),
- ...previewLines("SDK response (preview)", r.sdkResponsePreview),
- `**Missing in SDK (present in API) — ${r.missingInSDK.length}**`,
- "",
- ...bulletListOrNone(r.missingInSDK),
- "",
- `**Missing in API (present in SDK) — ${r.missingInAPI.length}**`,
- "",
- ...bulletListOrNone(r.missingInAPI),
- "",
- `**Empty arrays omitted by SDK — ${r.emptyArraysOmittedInSDK.length}**`,
- "",
- ...bulletListOrNone(r.emptyArraysOmittedInSDK),
- "",
- `**Empty arrays omitted by API — ${r.emptyArraysOmittedInAPI.length}**`,
- "",
- ...bulletListOrNone(r.emptyArraysOmittedInAPI),
- "",
- );
- return lines;
-}
-
-function consolidatedSection(
- results: EndpointResult[],
- generatedAt: string,
- total: number,
- passed: number,
- failed: number,
- skipped: number,
-): string[] {
- const consolidated: string[] = [];
- consolidated.push(
- `Last generated: ${generatedAt}`,
- "",
- `- **Total GET endpoints**: ${total}`,
- `- **PASS**: ${passed}`,
- `- **FAIL**: ${failed}`,
- `- **SKIP**: ${skipped}`,
- "",
- ...CONSOLIDATED_TABLE_HEADER,
- );
- for (const r of results) consolidated.push(consolidatedRow(r));
- consolidated.push("", "#### Missing fields (full lists)", "");
- for (const r of results) {
- consolidated.push(
- `- **${r.operationId}** (\`${r.endpoint}\`)`,
- ` - **Missing in SDK (present in API)**: ${joinOrNone(r.missingInSDK)}`,
- ` - **Missing in API (present in SDK)**: ${joinOrNone(r.missingInAPI)}`,
- ` - **Empty arrays omitted by SDK**: ${joinOrNone(r.emptyArraysOmittedInSDK)}`,
- ` - **Empty arrays omitted by API**: ${joinOrNone(r.emptyArraysOmittedInAPI)}`,
- );
- }
- consolidated.push(
- "",
- `Full details: \`tests/GET_ENDPOINTS_OPENAPI_RESPONSE_VALIDATION_REPORT.md\``,
- );
- return consolidated;
-}
-
-function updateReadmeConsolidated(readmePath: string, consolidated: string[]): void {
- // Keep tests/README.md's consolidated section in sync with the report.
- try {
- if (!existsSync(readmePath)) return;
- const begin = "";
- const end = "";
- const readme = readFileSync(readmePath, "utf-8");
- if (readme.includes(begin) && readme.includes(end)) {
- const block = `${begin}\n${consolidated.join("\n")}\n${end}`;
- const updated = readme.replace(new RegExp(String.raw`${begin}[\s\S]*?${end}`), block);
- writeFileSync(readmePath, updated);
- }
- } catch {
- // ignore README update failures
- }
-}
-
-function writeReport(results: EndpointResult[]) {
- const total = results.length;
- const passed = results.filter((r) => r.status === "PASS").length;
- const failed = results.filter((r) => r.status === "FAIL").length;
- const skipped = 0;
-
- const reportPath = join(__dirname, "GET_ENDPOINTS_OPENAPI_RESPONSE_VALIDATION_REPORT.md");
- const readmePath = join(__dirname, "README.md");
- const generatedAt = new Date().toISOString();
-
- const lines: string[] = [];
- lines.push(
- "# GET Endpoints — OpenAPI Response Validation Report",
- "",
- `Generated: ${generatedAt}`,
- "",
- "## Summary",
- "",
- `- **Total GET endpoints**: ${total}`,
- `- **PASS**: ${passed}`,
- `- **FAIL**: ${failed}`,
- `- **SKIP**: ${skipped}`,
- "",
- "## Consolidated report",
- "",
- ...CONSOLIDATED_TABLE_HEADER,
- );
- for (const r of results) lines.push(consolidatedRow(r));
- lines.push("", "## Per-endpoint details (full missing parameter lists)", "");
- for (const r of results) lines.push(...endpointDetailLines(r));
-
- writeFileSync(reportPath, lines.join("\n"));
- writeFixSuggestions(results);
-
- const consolidated = consolidatedSection(
- results, generatedAt, total, passed, failed, skipped,
- );
- updateReadmeConsolidated(readmePath, consolidated);
-
- // eslint-disable-next-line no-console
- console.log(`Report generated: ${reportPath}`);
- // eslint-disable-next-line no-console
- console.log(`Fix suggestions generated: ${join(__dirname, FIX_SUGGESTIONS_MD)}`);
- // eslint-disable-next-line no-console
- console.log(`Summary: total=${total} pass=${passed} fail=${failed} skip=${skipped}`);
-}
-
-async function fetchRawResponse(
- url: string,
- username: string,
- password: string,
-): Promise<{ httpStatus: number; rawBody: any; requestError?: string }> {
- let httpStatus = 0;
- let rawBody: any = null;
- let requestError: string | undefined;
- try {
- // Add timeout to prevent hanging
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
-
- const res = await fetch(url, {
- method: "GET",
- headers: {
- Accept: "application/json",
- Authorization: basicAuthHeader(username, password),
- },
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
-
- httpStatus = res.status;
- const bodyText = await res.text();
- try {
- rawBody = bodyText ? JSON.parse(bodyText) : null;
- } catch {
- rawBody = bodyText;
- }
- } catch (e: any) {
- if (e.name === 'AbortError') {
- requestError = "Request timeout (30s)";
- } else {
- requestError = e?.message ?? String(e);
- }
- // eslint-disable-next-line no-console
- console.error(` ⚠️ API request failed: ${requestError}`);
- }
- return { httpStatus, rawBody, requestError };
-}
-
-function validateOpenapi(
- spec: any,
- ep: EndpointInfo,
- requestError: string | undefined,
- httpStatus: number,
- rawBody: any,
-): { openapiValid: boolean; openapiErrors: any[] } {
- const validator = makeOpenAPIResponseValidator(spec, ep);
- if (requestError) {
- return { openapiValid: false, openapiErrors: [{ message: `Request failed: ${requestError}` }] };
- }
- if (!validator) return { openapiValid: true, openapiErrors: [] };
- const err = validator.validateResponse(String(httpStatus), rawBody);
- if (err) return { openapiValid: false, openapiErrors: err.errors || [] };
- return { openapiValid: true, openapiErrors: [] };
-}
-
-function diffPaths(
- rawBody: any,
- sdkValueForDiff: any,
-): {
- missingInSDK: string[];
- missingInAPI: string[];
- emptyArraysOmittedInSDK: string[];
- emptyArraysOmittedInAPI: string[];
-} {
- const apiNormalized = normalizeJsonForComparison(rawBody);
- const sdkJsonLike =
- (sdkValueForDiff && typeof sdkValueForDiff === "object")
- ? cloneJsonValue(sdkValueForDiff)
- : null;
- const sdkNormalized = sdkJsonLike ? normalizeJsonForComparison(sdkJsonLike) : null;
-
- // Treat `[]` the same as "missing" for comparison.
- const apiPaths = collectJsonPaths(apiNormalized, "", { includeEmptyArrays: false });
- const sdkPaths = sdkNormalized ? collectJsonPaths(sdkNormalized, "", { includeEmptyArrays: false }) : new Set();
-
- const missingInSDK = sdkPaths.size
- ? sortUnique([...apiPaths].filter((p) => !sdkPaths.has(p)))
- : [];
- const missingInAPI = sdkPaths.size
- ? sortUnique([...sdkPaths].filter((p) => !apiPaths.has(p)))
- : [];
-
- const apiStrictPaths = collectJsonPaths(apiNormalized, "", { includeEmptyArrays: true });
- const sdkStrictPaths = sdkNormalized ? collectJsonPaths(sdkNormalized, "", { includeEmptyArrays: true }) : new Set();
- const apiEmptyArrayFields = collectEmptyArrayFieldPaths(apiNormalized);
- const sdkEmptyArrayFields = sdkNormalized ? collectEmptyArrayFieldPaths(sdkNormalized) : new Set();
-
- const emptyArraysOmittedInSDK = sortUnique([...apiEmptyArrayFields].filter((p) => !sdkStrictPaths.has(p)));
- const emptyArraysOmittedInAPI = sortUnique([...sdkEmptyArrayFields].filter((p) => !apiStrictPaths.has(p)));
-
- return { missingInSDK, missingInAPI, emptyArraysOmittedInSDK, emptyArraysOmittedInAPI };
-}
-
-async function processEndpoint(
- spec: any,
- ep: EndpointInfo,
- fixtures: Fixture | null,
- baseUrl: string,
- username: string,
- password: string,
-): Promise {
- try {
- const { url, note } = buildUrl(baseUrl, ep, fixtures);
-
- const { httpStatus, rawBody, requestError } = await fetchRawResponse(url, username, password);
- const { openapiValid, openapiErrors } = validateOpenapi(spec, ep, requestError, httpStatus, rawBody);
-
- // SDK output: call SDK and capture success result or thrown error (normalized).
- const sdkReq = buildSDKRequest(ep, fixtures);
- let sdkParseOk = true;
- let sdkParseError: string | undefined;
- let sdkPrinted: any = null;
- let sdkValueForDiff: any = null;
-
- const py = invokePythonSDK(ep.operationId, sdkReq, baseUrl, username, password);
- if (py.ok) {
- sdkValueForDiff = py.value;
- sdkPrinted = py.value;
- } else {
- sdkParseOk = false;
- sdkParseError = py.error?.message ?? "Python SDK call failed";
- sdkPrinted = py.error;
- // eslint-disable-next-line no-console
- console.error(` ⚠️ Python SDK call failed: ${sdkParseError}`);
- }
-
- const diff = diffPaths(rawBody, sdkValueForDiff);
- const pass = openapiValid && sdkParseOk && diff.missingInSDK.length === 0 && diff.missingInAPI.length === 0;
-
- const artifacts = writeArtifactFiles(
- ep.operationId,
- rawBody,
- sdkPrinted,
- );
-
- const result: EndpointResult = {
- endpoint: ep.path,
- operationId: ep.operationId,
- method: "GET",
- openapiValid,
- openapiErrors,
- sdkParseOk,
- sdkParseError,
- ...diff,
- apiResponseFile: artifacts.apiPath,
- sdkResponseFile: artifacts.sdkPath,
- apiResponsePreview: artifacts.apiPreview,
- sdkResponsePreview: artifacts.sdkPreview,
- status: pass ? "PASS" : "FAIL",
- note,
- fixSuggestions: undefined,
- };
-
- // eslint-disable-next-line no-console
- console.log(` ✓ Completed: ${ep.operationId} - ${result.status}`);
- return result;
- } catch (error: any) {
- // Catch any unexpected errors and continue with next endpoint
- // eslint-disable-next-line no-console
- console.error(` ✗ Unexpected error processing ${ep.operationId}:`, error?.message ?? String(error));
- return {
- endpoint: ep.path,
- operationId: ep.operationId,
- method: "GET",
- openapiValid: false,
- openapiErrors: [{ message: `Unexpected error: ${error?.message ?? String(error)}` }],
- sdkParseOk: false,
- sdkParseError: error?.message ?? String(error),
- missingInSDK: [],
- missingInAPI: [],
- emptyArraysOmittedInSDK: [],
- emptyArraysOmittedInAPI: [],
- status: "FAIL",
- note: "Unexpected error during processing",
- fixSuggestions: undefined,
- };
- }
-}
-
-async function main(): Promise {
- const spec = loadOpenAPISpec();
- const endpoints = extractGetEndpoints(spec);
- const fixtures = readFixtures();
-
- const baseUrl: string =
- process.env.FASTPIX_BASE_URL
- ?? ((spec.servers?.[0]?.url as string | undefined) ?? "https://api.fastpix.com/v1/");
-
- const username = process.env.FASTPIX_USERNAME ?? "";
- const password = process.env.FASTPIX_PASSWORD ?? "";
-
- if (!username || !password) {
- throw new Error("Missing FASTPIX_USERNAME / FASTPIX_PASSWORD env vars (BasicAuth)");
- }
-
- const results: EndpointResult[] = [];
- const totalEndpoints = endpoints.length;
-
- for (let i = 0; i < endpoints.length; i++) {
- const ep = endpoints[i];
- // eslint-disable-next-line no-console
- console.log(`[${i + 1}/${totalEndpoints}] Processing: ${ep.operationId} (${ep.path})`);
- results.push(await processEndpoint(spec, ep, fixtures, baseUrl, username, password));
- }
-
- for (const r of results) {
- if (r.status !== "FAIL") continue;
- r.fixSuggestions = generateFixSuggestions(r);
- }
-
- writeReport(results);
-}
-
-await main();
-
diff --git a/tests/validate_non_get_endpoints.py b/tests/validate_non_get_endpoints.py
index c3bb09e..e891782 100644
--- a/tests/validate_non_get_endpoints.py
+++ b/tests/validate_non_get_endpoints.py
@@ -309,6 +309,14 @@ def _capture_upload(v, c):
retry_on=NOT_READY_SUBSTR,
request=lambda c: {"media_id": c["media_id"], "playback_id": c["media_playback_id"]},
body={"default_policy": "allow", "allow": [], "deny": []}),
+ Step("update-live-stream-domain-restrictions", "UPDATE",
+ needs=("stream_id", "stream_playback_id"),
+ request=lambda c: {"stream_id": c["stream_id"], "playback_id": c["stream_playback_id"]},
+ body={"default_policy": "allow", "allow": [], "deny": []}),
+ Step("update-live-stream-user-agent-restrictions", "UPDATE",
+ needs=("stream_id", "stream_playback_id"),
+ request=lambda c: {"stream_id": c["stream_id"], "playback_id": c["stream_playback_id"]},
+ body={"default_policy": "allow", "allow": [], "deny": []}),
Step("update-a-playlist", "UPDATE",
needs=("playlist_id",),
request=lambda c: {"playlist_id": c["playlist_id"]},
@@ -780,7 +788,16 @@ def main() -> int:
timeout=180.0,
)
security = models.Security(username=user, password=pwd)
- sdk = fastpix_cls(security=security, client=client)
+ sdk_kwargs: Dict[str, Any] = {"security": security, "client": client}
+ if os.environ.get("FASTPIX_BASE_URL"):
+ sdk_kwargs["server_url"] = os.environ["FASTPIX_BASE_URL"].rstrip("/")
+ # Cap retries: the SDK's per-method default backoff runs up to an hour on
+ # persistent 429/5xx, which stalls the harness on a single broken endpoint.
+ from fastpix_python.utils import BackoffStrategy, RetryConfig
+ sdk_kwargs["retry_config"] = RetryConfig(
+ "backoff", BackoffStrategy(1000, 10000, 1.5, 30000), False
+ )
+ sdk = fastpix_cls(**sdk_kwargs)
ctx: Dict[str, Any] = {}
total = len(STEPS)