├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Makefile ├── README.md ├── elemental ├── __init__.py └── client.py ├── poetry.lock ├── pyproject.toml ├── scripts └── publish.sh ├── tests ├── client_test.py └── fixtures │ ├── fail_to_create_response.xml │ ├── fail_to_start_response.xml │ ├── fail_to_stop_response.xml │ ├── sample_device_list.xml │ ├── sample_event.xml │ ├── sample_event_list.xml │ ├── sample_single_device.xml │ ├── success_response_for_create.xml │ ├── success_response_for_delete.xml │ └── success_response_for_generate_preview.json └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: pytest 11 | versions: 12 | - 6.2.2 13 | - dependency-name: pytest-mypy 14 | versions: 15 | - 0.8.0 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | test: 15 | name: Run Tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v1 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 3.8 24 | - name: Install test dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install tox==3.20.0 poetry==1.1.3 28 | - name: Run Tests 29 | run: | 30 | tox 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | env: 8 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 9 | 10 | jobs: 11 | test: 12 | name: Publish to PyPI 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.8 21 | - name: Install test dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install poetry==1.1.3 25 | - name: Install Dependencies 26 | run: poetry install 27 | - name: Publish to PyPI 28 | run: | 29 | poetry config pypi-token.pypi ${{ env.PYPI_TOKEN }} 30 | bash scripts/publish.sh 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | .python-version 3 | build 4 | dist 5 | python_elemental.egg-info 6 | elemental/main.py 7 | elemental/Keys.py 8 | elemental/.DS_Store 9 | elemental/templates/.DS_Store 10 | python_elemental.egg-info 11 | venv 12 | .coverage 13 | .DS_Store 14 | 15 | __pycache__ 16 | .idea 17 | .coverage 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean-pyc: 2 | find . -name '*.pyc' -exec rm --force {} + 3 | find . -name '*.pyo' -exec rm --force {} + 4 | name '*~' -exec rm --force {} 5 | 6 | clean-build: 7 | rm --force --recursive build/ 8 | rm --force --recursive dist/ 9 | rm --force --recursive *.egg-info 10 | 11 | lint: 12 | tox -e deadfixtures,isort-check,flake8 13 | 14 | test: 15 | tox 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Elemental 3 | 4 | [![continuous integration status](https://github.com/cbsinteractive/elemental/workflows/CI/badge.svg)](https://github.com/cbsinteractive/elemental/actions?query=workflow%3ACI) 5 | [![codecov](https://codecov.io/gh/cbsinteractive/elemental/branch/master/graph/badge.svg?token=qFdUKsI2tD)](https://codecov.io/gh/cbsinteractive/elemental) 6 | 7 | 8 | Python Client for Elemental On-Premises Appliances 9 | 10 | ## Run Tests 11 | 12 | Before running tests locally, install `tox` and `poetry`. 13 | 14 | pipx install tox 15 | pipx install poetry 16 | 17 | Run tests using 18 | 19 | make test 20 | 21 | To run lint, use 22 | 23 | make lint 24 | 25 | ## Release Updated Version 26 | Use the Github UI to [create a new release](https://github.com/cbsinteractive/elemental/releases/new), the tag needs 27 | to follow the semver format `0.0.0`. After the new release is created, a Github workflow will build and publish the 28 | new python package to PyPI automatically. 29 | -------------------------------------------------------------------------------- /elemental/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import (ConnectionTimeout, ElementalException, ElementalLive, 2 | InvalidRequest, InvalidResponse, NotFound) 3 | 4 | __all__ = ('ConnectionTimeout', 'ElementalException', 'ElementalLive', 'InvalidResponse', 'InvalidRequest', 'NotFound') 5 | -------------------------------------------------------------------------------- /elemental/client.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import hashlib 3 | import time 4 | import xml.etree.ElementTree as ET 5 | from typing import Dict, List, Optional, Set, TypedDict 6 | from urllib.parse import urlparse 7 | 8 | import requests 9 | import xmltodict # type: ignore 10 | 11 | 12 | class ElementalException(Exception): 13 | """Base exception for all exceptions ElementalLive client could raise""" 14 | pass 15 | 16 | 17 | class InvalidRequest(ElementalException): 18 | """Exception raised by 'request' with invalid request""" 19 | pass 20 | 21 | 22 | class InvalidResponse(ElementalException): 23 | """Exception raised by 'request' with invalid response""" 24 | pass 25 | 26 | 27 | class NotFound(InvalidResponse): 28 | """Exception raised by 'request' with NotFound""" 29 | pass 30 | 31 | 32 | class ConnectionTimeout(ElementalException): 33 | """Exception raised by 'request' with invalid response""" 34 | pass 35 | 36 | 37 | EventIdDict = TypedDict('EventIdDict', {'id': str}) 38 | 39 | EventStatusDict = TypedDict('EventStatusDict', {'origin_url': str, 'backup_url': Optional[str], 'status': str}) 40 | 41 | DeviceAvailabilityDict = TypedDict('DeviceAvailabilityDict', { 42 | 'id': str, 43 | 'name': Optional[str], 44 | 'device_name': str, 45 | 'device_number': str, 46 | 'device_type': str, 47 | 'description': str, 48 | 'channel': str, 49 | 'channel_type': str, 50 | 'quad': str, 51 | 'availability': bool 52 | }) 53 | 54 | PreviewUrlDict = TypedDict('PreviewUrlDict', {'preview_url': str}) 55 | 56 | 57 | class ElementalLive: 58 | def __init__(self, server_url: str, user: Optional[str] = None, api_key: Optional[str] = None, 59 | timeout: Optional[int] = 5) -> None: 60 | self.server_url = server_url 61 | self.user = user 62 | self.api_key = api_key 63 | self.timeout = timeout 64 | self.session = requests.Session() 65 | 66 | def generate_headers(self, url: Optional[str] = "") -> Dict[str, str]: 67 | # Generate headers according to how users create ElementalLive class 68 | if self.user is None or self.api_key is None: 69 | return { 70 | 'Accept': 'application/xml', 71 | 'Content-Type': 'application/xml' 72 | } 73 | else: 74 | expiration = int(time.time() + 120) 75 | parse = urlparse(url) 76 | pre_hash = f"{str(parse.path)}{self.user}{self.api_key}{expiration}" 77 | digest = hashlib.md5(pre_hash.encode('utf-8')).hexdigest() 78 | final_hash = f"{self.api_key}{digest}" 79 | key = hashlib.md5(final_hash.encode('utf-8')).hexdigest() 80 | 81 | return { 82 | 'X-Auth-User': str(self.user), 83 | 'X-Auth-Expires': str(expiration), 84 | 'X-Auth-Key': key, 85 | 'Accept': 'application/xml', 86 | 'Content-Type': 'application/xml' 87 | } 88 | 89 | def send_request(self, http_method: str, url: str, headers: Dict[str, str], 90 | body: Optional[str] = "", timeout: Optional[int] = None) -> requests.Response: 91 | # Send request according to different methods 92 | try: 93 | timeout = timeout or self.timeout 94 | response = self.session.request( 95 | method=http_method, url=url, data=body, headers=headers, timeout=timeout) 96 | 97 | except requests.exceptions.ConnectTimeout as e: 98 | raise ConnectionTimeout(f"{http_method}: {url} failed\n{e}") 99 | except requests.exceptions.RequestException as e: 100 | raise InvalidRequest(f"{http_method}: {url} failed\n{e}") 101 | if response.status_code == 404: 102 | raise NotFound( 103 | f"{http_method}: {url} failed\nResponse: " 104 | f"{response.status_code}\n{response.text}") 105 | if response.status_code not in (200, 201): 106 | raise InvalidResponse( 107 | f"{http_method}: {url} failed\nResponse: " 108 | f"{response.status_code}\n{response.text}") 109 | return response 110 | 111 | def create_event(self, event_xml: str, timeout: Optional[int] = None) -> EventIdDict: 112 | url = f'{self.server_url}/live_events' 113 | headers = self.generate_headers(url) 114 | response = self.send_request( 115 | http_method="POST", url=url, headers=headers, body=event_xml, timeout=timeout) 116 | xml_root = ET.fromstring(response.content) 117 | ids = xml_root.findall('id') 118 | event_id = str(ids[0].text) 119 | 120 | return {'id': event_id} 121 | 122 | def update_event(self, event_id: str, event_xml: str, restart: Optional[bool] = False, 123 | timeout: Optional[int] = None) -> None: 124 | url = f'{self.server_url}/live_events/{event_id}' 125 | if restart: 126 | url += '?unlocked=1' 127 | headers = self.generate_headers(url) 128 | self.send_request( 129 | http_method="PUT", url=url, headers=headers, body=event_xml, timeout=timeout) 130 | 131 | def delete_event(self, event_id: str, timeout: Optional[int] = None) -> None: 132 | url = f'{self.server_url}/live_events/{event_id}' 133 | headers = self.generate_headers(url) 134 | self.send_request(http_method="DELETE", url=url, headers=headers, timeout=timeout) 135 | 136 | def cancel_event(self, event_id: str, timeout: Optional[int] = None) -> None: 137 | url = f'{self.server_url}/live_events/{event_id}/cancel' 138 | headers = self.generate_headers(url) 139 | self.send_request(http_method="POST", url=url, headers=headers, timeout=timeout) 140 | 141 | def start_event(self, event_id: str, timeout: Optional[int] = None) -> None: 142 | url = f'{self.server_url}/live_events/{event_id}/start' 143 | body = "" 144 | headers = self.generate_headers(url) 145 | self.send_request(http_method="POST", url=url, headers=headers, body=body, timeout=timeout) 146 | 147 | def stop_event(self, event_id: str, timeout: Optional[int] = None) -> None: 148 | url = f'{self.server_url}/live_events/{event_id}/stop' 149 | body = "" 150 | headers = self.generate_headers(url) 151 | self.send_request(http_method="POST", url=url, headers=headers, body=body, timeout=timeout) 152 | 153 | def event_pause_output(self, event_id: str, output_id: str, timeout: Optional[int] = None) -> None: 154 | url = f'{self.server_url}/live_events/{event_id}/pause_output' 155 | body = f"{output_id}" 156 | headers = self.generate_headers(url) 157 | self.send_request(http_method="POST", url=url, headers=headers, body=body, timeout=timeout) 158 | 159 | def event_unpause_output(self, event_id: str, output_id: str, timeout: Optional[int] = None) -> None: 160 | url = f'{self.server_url}/live_events/{event_id}/unpause_output' 161 | body = f"{output_id}" 162 | headers = self.generate_headers(url) 163 | self.send_request(http_method="POST", url=url, headers=headers, body=body, timeout=timeout) 164 | 165 | def event_start_output(self, event_id: str, output_id: str, timeout: Optional[int] = None) -> None: 166 | url = f'{self.server_url}/live_events/{event_id}/start_output' 167 | body = f"{output_id}" 168 | headers = self.generate_headers(url) 169 | self.send_request(http_method="POST", url=url, headers=headers, body=body, timeout=timeout) 170 | 171 | def event_stop_output(self, event_id: str, output_id: str, timeout: Optional[int] = None) -> None: 172 | url = f'{self.server_url}/live_events/{event_id}/stop_output' 173 | body = f"{output_id}" 174 | headers = self.generate_headers(url) 175 | self.send_request(http_method="POST", url=url, headers=headers, body=body, timeout=timeout) 176 | 177 | def reset_event(self, event_id: str, timeout: Optional[int] = None) -> None: 178 | url = f'{self.server_url}/live_events/{event_id}/reset' 179 | headers = self.generate_headers(url) 180 | self.send_request(http_method="POST", url=url, headers=headers, body="", timeout=timeout) 181 | 182 | def describe_event(self, event_id: str, timeout: Optional[int] = None) -> EventStatusDict: 183 | url = f'{self.server_url}/live_events/{event_id}' 184 | headers = self.generate_headers(url) 185 | response = self.send_request(http_method="GET", url=url, 186 | headers=headers, timeout=timeout) 187 | event_info = {} 188 | 189 | destinations = list(ET.fromstring(response.text).iter('destination')) 190 | uri = destinations[0].find('uri') if destinations else None 191 | event_info['origin_url'] = uri.text if uri is not None else '' 192 | if len(destinations) > 1: 193 | uri = destinations[1].find('uri') 194 | event_info['backup_url'] = uri.text if uri is not None else '' 195 | 196 | event_info['status'] = self._parse_status(response.text) 197 | 198 | return EventStatusDict( 199 | status=str(event_info['status']), 200 | origin_url=str(event_info['origin_url']), 201 | backup_url=event_info.get('backup_url') 202 | ) 203 | 204 | def get_event_xml(self, event_id: str, timeout: Optional[int] = None) -> str: 205 | url = f'{self.server_url}/live_events/{event_id}' 206 | headers = self.generate_headers(url) 207 | response = self.send_request(http_method="GET", url=url, 208 | headers=headers, timeout=timeout) 209 | return response.text 210 | 211 | def get_event_status(self, event_id: str, timeout: Optional[int] = None) -> str: 212 | url = f'{self.server_url}/live_events/{event_id}/status' 213 | headers = self.generate_headers(url) 214 | response = self.send_request(http_method="GET", url=url, headers=headers, timeout=timeout) 215 | return self._parse_status(response.text) 216 | 217 | def find_devices_in_use(self, timeout: Optional[int] = None) -> Set[Optional[str]]: 218 | events_url = f'{self.server_url}/live_events?filter=active' 219 | events_headers = self.generate_headers(events_url) 220 | events = self.send_request( 221 | http_method="GET", url=events_url, headers=events_headers, timeout=timeout) 222 | events_list = ET.fromstring(events.text) 223 | 224 | # Find in use devices from active events 225 | in_use_devices = set() 226 | for device_name in events_list.iter('device_name'): 227 | in_use_devices.add(device_name.text) 228 | 229 | return in_use_devices 230 | 231 | def get_input_devices(self, timeout: Optional[int] = None) -> List[DeviceAvailabilityDict]: 232 | devices_url = f'{self.server_url}/devices' 233 | devices_headers = self.generate_headers(devices_url) 234 | devices = self.send_request( 235 | http_method="GET", url=devices_url, headers=devices_headers, timeout=timeout) 236 | devices_information = xmltodict.parse(devices.text)[ 237 | 'device_list']['device'] 238 | 239 | devices_in_use = self.find_devices_in_use() 240 | 241 | for device in devices_information: 242 | device.pop('@href') 243 | device['availability'] = \ 244 | (device['device_name'] not in devices_in_use) 245 | 246 | devices_information = sorted(devices_information, key=lambda d: int(d["id"])) 247 | return [DeviceAvailabilityDict( 248 | id=device_info['id'], 249 | name=device_info['name'], 250 | description=device_info['description'], 251 | device_name=device_info['device_name'], 252 | device_number=device_info['device_number'], 253 | device_type=device_info['device_type'], 254 | availability=device_info['availability'], 255 | channel=device_info['channel'], 256 | channel_type=device_info['channel_type'], 257 | quad=device_info['quad'], 258 | ) for device_info in devices_information] 259 | 260 | def get_input_device_by_id(self, input_device_id: str, timeout: Optional[int] = None) -> DeviceAvailabilityDict: 261 | devices_url = f'{self.server_url}/devices/{input_device_id}' 262 | devices_headers = self.generate_headers(devices_url) 263 | devices = self.send_request( 264 | http_method="GET", url=devices_url, headers=devices_headers, timeout=timeout) 265 | device_info = xmltodict.parse(devices.text)['device'] 266 | devices_in_use = self.find_devices_in_use() 267 | device_info['availability'] = (device_info['device_name'] 268 | not in devices_in_use) 269 | device_info.pop('@href') 270 | return DeviceAvailabilityDict( 271 | id=device_info['id'], 272 | name=device_info['name'], 273 | description=device_info['description'], 274 | device_name=device_info['device_name'], 275 | device_number=device_info['device_number'], 276 | device_type=device_info['device_type'], 277 | availability=device_info['availability'], 278 | channel=device_info['channel'], 279 | channel_type=device_info['channel_type'], 280 | quad=device_info['quad'], 281 | ) 282 | 283 | def generate_preview(self, input_id: str, timeout: Optional[int] = None) -> PreviewUrlDict: 284 | url = f'{self.server_url}/inputs/generate_preview' 285 | headers = self.generate_headers(url) 286 | 287 | headers['Accept'] = '*/*' 288 | headers['Content-Type'] = 'application/x-www-form-urlencoded; ' \ 289 | 'charset=UTF-8' 290 | 291 | # generate body 292 | data = f"input_key=0&live_event[inputs_attributes][0][source_type]=" \ 293 | f"DeviceInput&live_event[inputs_attributes][0]" \ 294 | f"[device_input_attributes][sdi_settings_attributes]" \ 295 | f"[input_format]=Auto&live_event[inputs_attributes][0]" \ 296 | f"[device_input_attributes][device_id]={input_id}" 297 | response = self.send_request( 298 | http_method="POST", url=url, headers=headers, body=data, timeout=timeout) 299 | 300 | response_parse = ast.literal_eval(response.text) 301 | 302 | if 'type' in response_parse and response_parse['type'] == 'error': 303 | raise ElementalException( 304 | f"Response: {response.status_code}\n{response.text}") 305 | else: 306 | preview_url = f'{self.server_url}/images/thumbs/' \ 307 | f'p_{response_parse["preview_image_id"]}_job_0.jpg' 308 | return {'preview_url': preview_url} 309 | 310 | def event_can_delete(self, channel_id: str, timeout: Optional[int] = None) -> bool: 311 | channel_info = self.describe_event(channel_id, timeout=timeout) 312 | return channel_info['status'] not in ('pending', 'running', 'preprocessing', 'postprocessing',) 313 | 314 | def _parse_status(self, text): 315 | status = ET.fromstring(text).find('status') 316 | return status.text if status is not None else 'unknown' 317 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "20.2.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] 19 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 22 | 23 | [[package]] 24 | name = "certifi" 25 | version = "2020.6.20" 26 | description = "Python package for providing Mozilla's CA Bundle." 27 | category = "main" 28 | optional = false 29 | python-versions = "*" 30 | 31 | [[package]] 32 | name = "charset-normalizer" 33 | version = "2.0.3" 34 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 35 | category = "main" 36 | optional = false 37 | python-versions = ">=3.5.0" 38 | 39 | [package.extras] 40 | unicode_backport = ["unicodedata2"] 41 | 42 | [[package]] 43 | name = "colorama" 44 | version = "0.4.4" 45 | description = "Cross-platform colored terminal text." 46 | category = "dev" 47 | optional = false 48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 49 | 50 | [[package]] 51 | name = "coverage" 52 | version = "5.3" 53 | description = "Code coverage measurement for Python" 54 | category = "dev" 55 | optional = false 56 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 57 | 58 | [package.extras] 59 | toml = ["toml"] 60 | 61 | [[package]] 62 | name = "filelock" 63 | version = "3.0.12" 64 | description = "A platform independent file lock." 65 | category = "dev" 66 | optional = false 67 | python-versions = "*" 68 | 69 | [[package]] 70 | name = "idna" 71 | version = "2.10" 72 | description = "Internationalized Domain Names in Applications (IDNA)" 73 | category = "main" 74 | optional = false 75 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 76 | 77 | [[package]] 78 | name = "iniconfig" 79 | version = "1.1.1" 80 | description = "iniconfig: brain-dead simple config-ini parsing" 81 | category = "dev" 82 | optional = false 83 | python-versions = "*" 84 | 85 | [[package]] 86 | name = "mypy" 87 | version = "0.790" 88 | description = "Optional static typing for Python" 89 | category = "dev" 90 | optional = false 91 | python-versions = ">=3.5" 92 | 93 | [package.dependencies] 94 | mypy-extensions = ">=0.4.3,<0.5.0" 95 | typed-ast = ">=1.4.0,<1.5.0" 96 | typing-extensions = ">=3.7.4" 97 | 98 | [package.extras] 99 | dmypy = ["psutil (>=4.0)"] 100 | 101 | [[package]] 102 | name = "mypy-extensions" 103 | version = "0.4.3" 104 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 105 | category = "dev" 106 | optional = false 107 | python-versions = "*" 108 | 109 | [[package]] 110 | name = "packaging" 111 | version = "20.4" 112 | description = "Core utilities for Python packages" 113 | category = "dev" 114 | optional = false 115 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 116 | 117 | [package.dependencies] 118 | pyparsing = ">=2.0.2" 119 | six = "*" 120 | 121 | [[package]] 122 | name = "pluggy" 123 | version = "0.13.1" 124 | description = "plugin and hook calling mechanisms for python" 125 | category = "dev" 126 | optional = false 127 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 128 | 129 | [package.extras] 130 | dev = ["pre-commit", "tox"] 131 | 132 | [[package]] 133 | name = "py" 134 | version = "1.10.0" 135 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 136 | category = "dev" 137 | optional = false 138 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 139 | 140 | [[package]] 141 | name = "pyparsing" 142 | version = "2.4.7" 143 | description = "Python parsing module" 144 | category = "dev" 145 | optional = false 146 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 147 | 148 | [[package]] 149 | name = "pytest" 150 | version = "6.2.4" 151 | description = "pytest: simple powerful testing with Python" 152 | category = "dev" 153 | optional = false 154 | python-versions = ">=3.6" 155 | 156 | [package.dependencies] 157 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 158 | attrs = ">=19.2.0" 159 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 160 | iniconfig = "*" 161 | packaging = "*" 162 | pluggy = ">=0.12,<1.0.0a1" 163 | py = ">=1.8.2" 164 | toml = "*" 165 | 166 | [package.extras] 167 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 168 | 169 | [[package]] 170 | name = "pytest-cov" 171 | version = "2.12.1" 172 | description = "Pytest plugin for measuring coverage." 173 | category = "dev" 174 | optional = false 175 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 176 | 177 | [package.dependencies] 178 | coverage = ">=5.2.1" 179 | pytest = ">=4.6" 180 | toml = "*" 181 | 182 | [package.extras] 183 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 184 | 185 | [[package]] 186 | name = "pytest-mypy" 187 | version = "0.7.0" 188 | description = "Mypy static type checker plugin for Pytest" 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=3.5" 192 | 193 | [package.dependencies] 194 | filelock = ">=3.0" 195 | mypy = {version = ">=0.700", markers = "python_version >= \"3.8\""} 196 | pytest = ">=3.5" 197 | 198 | [[package]] 199 | name = "requests" 200 | version = "2.26.0" 201 | description = "Python HTTP for Humans." 202 | category = "main" 203 | optional = false 204 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 205 | 206 | [package.dependencies] 207 | certifi = ">=2017.4.17" 208 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 209 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 210 | urllib3 = ">=1.21.1,<1.27" 211 | 212 | [package.extras] 213 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 214 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 215 | 216 | [[package]] 217 | name = "requests-mock" 218 | version = "1.9.3" 219 | description = "Mock out responses from the requests package" 220 | category = "dev" 221 | optional = false 222 | python-versions = "*" 223 | 224 | [package.dependencies] 225 | requests = ">=2.3,<3" 226 | six = "*" 227 | 228 | [package.extras] 229 | fixture = ["fixtures"] 230 | test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.18)", "testtools"] 231 | 232 | [[package]] 233 | name = "six" 234 | version = "1.15.0" 235 | description = "Python 2 and 3 compatibility utilities" 236 | category = "dev" 237 | optional = false 238 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 239 | 240 | [[package]] 241 | name = "toml" 242 | version = "0.10.1" 243 | description = "Python Library for Tom's Obvious, Minimal Language" 244 | category = "dev" 245 | optional = false 246 | python-versions = "*" 247 | 248 | [[package]] 249 | name = "typed-ast" 250 | version = "1.4.1" 251 | description = "a fork of Python 2 and 3 ast modules with type comment support" 252 | category = "dev" 253 | optional = false 254 | python-versions = "*" 255 | 256 | [[package]] 257 | name = "typing-extensions" 258 | version = "3.7.4.3" 259 | description = "Backported and Experimental Type Hints for Python 3.5+" 260 | category = "dev" 261 | optional = false 262 | python-versions = "*" 263 | 264 | [[package]] 265 | name = "urllib3" 266 | version = "1.25.11" 267 | description = "HTTP library with thread-safe connection pooling, file post, and more." 268 | category = "main" 269 | optional = false 270 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 271 | 272 | [package.extras] 273 | brotli = ["brotlipy (>=0.6.0)"] 274 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 275 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 276 | 277 | [[package]] 278 | name = "xmltodict" 279 | version = "0.12.0" 280 | description = "Makes working with XML feel like you are working with JSON" 281 | category = "main" 282 | optional = false 283 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 284 | 285 | [metadata] 286 | lock-version = "1.1" 287 | python-versions = "^3.8" 288 | content-hash = "a9e780692115af4268bd9ded797053cb9e2fb3f356ec51378ee57f4eb6da8ef3" 289 | 290 | [metadata.files] 291 | atomicwrites = [ 292 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 293 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 294 | ] 295 | attrs = [ 296 | {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, 297 | {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, 298 | ] 299 | certifi = [ 300 | {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, 301 | {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, 302 | ] 303 | charset-normalizer = [ 304 | {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"}, 305 | {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"}, 306 | ] 307 | colorama = [ 308 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 309 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 310 | ] 311 | coverage = [ 312 | {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, 313 | {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, 314 | {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, 315 | {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, 316 | {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, 317 | {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, 318 | {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, 319 | {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, 320 | {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, 321 | {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, 322 | {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, 323 | {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, 324 | {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, 325 | {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, 326 | {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, 327 | {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, 328 | {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, 329 | {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, 330 | {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, 331 | {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, 332 | {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, 333 | {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, 334 | {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, 335 | {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, 336 | {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, 337 | {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, 338 | {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, 339 | {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, 340 | {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, 341 | {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, 342 | {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, 343 | {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, 344 | {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, 345 | {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, 346 | ] 347 | filelock = [ 348 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 349 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 350 | ] 351 | idna = [ 352 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 353 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 354 | ] 355 | iniconfig = [ 356 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 357 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 358 | ] 359 | mypy = [ 360 | {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, 361 | {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, 362 | {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, 363 | {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, 364 | {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, 365 | {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, 366 | {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, 367 | {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, 368 | {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, 369 | {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, 370 | {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, 371 | {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, 372 | {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, 373 | {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, 374 | ] 375 | mypy-extensions = [ 376 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 377 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 378 | ] 379 | packaging = [ 380 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 381 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 382 | ] 383 | pluggy = [ 384 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 385 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 386 | ] 387 | py = [ 388 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 389 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 390 | ] 391 | pyparsing = [ 392 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 393 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 394 | ] 395 | pytest = [ 396 | {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, 397 | {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, 398 | ] 399 | pytest-cov = [ 400 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 401 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 402 | ] 403 | pytest-mypy = [ 404 | {file = "pytest-mypy-0.7.0.tar.gz", hash = "sha256:5a667d9a2b66bf98b3a494411f221923a6e2c3eafbe771104951aaec8985673d"}, 405 | {file = "pytest_mypy-0.7.0-py3-none-any.whl", hash = "sha256:e0505ace48d2b19fe686366fce6b4a2ac0d090423736bb6aa2e39554d18974b7"}, 406 | ] 407 | requests = [ 408 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 409 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 410 | ] 411 | requests-mock = [ 412 | {file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"}, 413 | {file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"}, 414 | ] 415 | six = [ 416 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 417 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 418 | ] 419 | toml = [ 420 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 421 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 422 | ] 423 | typed-ast = [ 424 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 425 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 426 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 427 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 428 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 429 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 430 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 431 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, 432 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 433 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 434 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 435 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 436 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 437 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, 438 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 439 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 440 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 441 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 442 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 443 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, 444 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 445 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 446 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 447 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, 448 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, 449 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, 450 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, 451 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, 452 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, 453 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 454 | ] 455 | typing-extensions = [ 456 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 457 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 458 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 459 | ] 460 | urllib3 = [ 461 | {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, 462 | {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, 463 | ] 464 | xmltodict = [ 465 | {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"}, 466 | {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, 467 | ] 468 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-elemental" 3 | version = "0.6.0" 4 | description = "Python Client for Elemental On-Premises Appliances" 5 | authors = ["CBS Interactive "] 6 | repository = "https://github.com/cbsinteractive/elemental.git" 7 | readme = "README.md" 8 | license = "MIT" 9 | packages = [ 10 | { include = "elemental" }, 11 | ] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.8" 15 | requests = "^2.23" 16 | xmltodict = "^0.12.0" 17 | 18 | [tool.poetry.dev-dependencies] 19 | requests-mock = "^1.9.3" 20 | pytest = "^6.2.4" 21 | pytest-cov = "^2.12.1" 22 | pytest-mypy = "^0.7.0" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | version=$(git describe --tags --abbrev=0) 7 | poetry version "${version}" 8 | poetry publish --build 9 | -------------------------------------------------------------------------------- /tests/client_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from unittest import mock 5 | import pytest 6 | import requests 7 | 8 | from elemental.client import (ElementalException, ElementalLive, InvalidRequest, 9 | InvalidResponse, NotFound) 10 | 11 | USER = "FAKE" 12 | API_KEY = "FAKE" 13 | ELEMENTAL_ADDRESS = "FAKE_ADDRESS.com" 14 | HEADERS = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 15 | REQUEST_BODY = "FAKE" 16 | TIMEOUT = 10 17 | 18 | 19 | def file_fixture(file_name): 20 | with open(os.path.join("tests/fixtures", file_name)) as f: 21 | return f.read() 22 | 23 | 24 | def mock_response(status=200, content=None, text=None, 25 | json_data=None, raise_for_status=None): 26 | mock_resp = mock.Mock() 27 | mock_resp.raise_for_status = mock.Mock() 28 | 29 | # mock raise_for_status call w/optional error 30 | if raise_for_status: 31 | mock_resp.raise_for_status.side_effect = raise_for_status 32 | 33 | # set status code and content 34 | mock_resp.status_code = status 35 | mock_resp.content = content 36 | mock_resp.text = text 37 | 38 | # add json data if provided 39 | if json_data: 40 | mock_resp.json = mock.Mock(return_value=json_data) 41 | return mock_resp 42 | 43 | 44 | def test_ElementalLive_should_receive_server_ip(): 45 | e = ElementalLive(ELEMENTAL_ADDRESS) 46 | assert e.server_url == ELEMENTAL_ADDRESS 47 | 48 | 49 | def test_generate_header_with_authentication_should_contain_user(): 50 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 51 | headers = client.generate_headers(f'{ELEMENTAL_ADDRESS}/live_events') 52 | assert headers['X-Auth-User'] == USER 53 | assert headers['Accept'] == 'application/xml' 54 | assert headers['Content-Type'] == 'application/xml' 55 | 56 | 57 | def test_generate_header_without_authentication_should_not_contain_user(): 58 | client = ElementalLive(ELEMENTAL_ADDRESS) 59 | headers = client.generate_headers() 60 | assert 'X-Auth-User' not in headers 61 | assert headers['Accept'] == 'application/xml' 62 | assert headers['Content-Type'] == 'application/xml' 63 | 64 | 65 | def test_send_request_should_call_request_as_expected(): 66 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 67 | client.session.request = mock.MagicMock( 68 | return_value=mock_response(status=200)) 69 | client.send_request( 70 | 'POST', f'{ELEMENTAL_ADDRESS}/live_events', {'Accept': 'application/xml', 'Content-Type': 'application/xml'}, 71 | REQUEST_BODY, timeout=TIMEOUT) 72 | 73 | request_to_elemental = client.session.request.call_args_list[0][1] 74 | assert request_to_elemental['url'] == f'{ELEMENTAL_ADDRESS}/live_events' 75 | assert request_to_elemental['method'] == 'POST' 76 | assert request_to_elemental['headers']['Accept'] == 'application/xml' 77 | assert request_to_elemental['headers']['Content-Type'] == 'application/xml' 78 | assert request_to_elemental['timeout'] == TIMEOUT 79 | 80 | 81 | def test_send_request_should_return_response_on_correct_status_code(): 82 | response_from_elemental_api = file_fixture('success_response_for_create.xml') 83 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 84 | client.session.request = mock.MagicMock(return_value=mock_response( 85 | status=201, text=response_from_elemental_api)) 86 | response = client.send_request( 87 | 'POST', f'{ELEMENTAL_ADDRESS}/live_events', {'Accept': 'application/xml', 'Content-Type': 'application/xml'}, 88 | REQUEST_BODY) 89 | 90 | assert response.text == response_from_elemental_api 91 | assert response.status_code == 201 92 | 93 | 94 | def test_send_request_should_raise_InvalidRequest_on_RequestException(): 95 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 96 | client.session.request = mock.MagicMock( 97 | side_effect=requests.exceptions.RequestException()) 98 | 99 | with pytest.raises(InvalidRequest): 100 | client.send_request( 101 | 'POST', f'{ELEMENTAL_ADDRESS}/live_events', 102 | {'Accept': 'application/xml', 'Content-Type': 'application/xml'}, REQUEST_BODY) 103 | 104 | 105 | def test_send_request_should_raise_InvalidResponse_on_invalid_status_code(): 106 | response_from_elemental_api = file_fixture('fail_to_create_response.xml') 107 | 108 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 109 | client.session.request = mock.MagicMock(return_value=mock_response( 110 | status=404, text=response_from_elemental_api)) 111 | 112 | with pytest.raises(InvalidResponse) as exc_info: 113 | client.send_request( 114 | 'POST', f'{ELEMENTAL_ADDRESS}/live_events', 115 | {'Accept': 'application/xml', 'Content-Type': 'application/xml'}, REQUEST_BODY) 116 | 117 | assert str(exc_info.value).endswith( 118 | f"Response: 404\n{response_from_elemental_api}") 119 | 120 | 121 | def test_send_request_should_raise_NotFound_on_404(): 122 | response_from_elemental_api = file_fixture('fail_to_create_response.xml') 123 | 124 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 125 | client.session.request = mock.MagicMock(return_value=mock_response( 126 | status=404, text=response_from_elemental_api)) 127 | 128 | with pytest.raises(NotFound) as exc_info: 129 | client.send_request( 130 | 'POST', f'{ELEMENTAL_ADDRESS}/live_events', HEADERS, REQUEST_BODY) 131 | 132 | assert str(exc_info.value).endswith( 133 | f"Response: 404\n{response_from_elemental_api}") 134 | 135 | 136 | def test_create_event(): 137 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 138 | 139 | client.generate_headers = mock.Mock() 140 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 141 | 142 | client.send_request = mock.Mock() 143 | elemental_response = file_fixture('success_response_for_create.xml') 144 | 145 | client.send_request.return_value = mock_response( 146 | status=201, content=elemental_response) 147 | 148 | event_id = client.create_event('') 149 | 150 | client.send_request.assert_called_once_with( 151 | http_method='POST', url='FAKE_ADDRESS.com/live_events', 152 | headers={'Accept': 'application/xml', 153 | 'Content-Type': 'application/xml'}, 154 | body='', timeout=None) 155 | 156 | send_mock_call = client.send_request.call_args_list[0][1] 157 | assert send_mock_call['http_method'] == 'POST' 158 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events' 159 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 160 | assert event_id == {'id': '53'} 161 | 162 | 163 | def test_update_event(): 164 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 165 | client.generate_headers = mock.Mock() 166 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 167 | client.send_request = mock.Mock() 168 | client.send_request.return_value = mock_response( 169 | status=200) 170 | 171 | client.update_event('53', '') 172 | 173 | client.send_request.assert_called_once_with( 174 | http_method='PUT', url='FAKE_ADDRESS.com/live_events/53', 175 | headers={'Accept': 'application/xml', 176 | 'Content-Type': 'application/xml'}, 177 | body='', timeout=None) 178 | send_mock_call = client.send_request.call_args_list[0][1] 179 | assert send_mock_call['http_method'] == 'PUT' 180 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events/53' 181 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 182 | 183 | 184 | def test_update_event_with_restart(): 185 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 186 | client.generate_headers = mock.Mock() 187 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 188 | client.send_request = mock.Mock() 189 | client.send_request.return_value = mock_response( 190 | status=200) 191 | 192 | client.update_event('53', '', restart=True) 193 | 194 | client.send_request.assert_called_once_with( 195 | http_method='PUT', url='FAKE_ADDRESS.com/live_events/53?unlocked=1', 196 | headers={'Accept': 'application/xml', 197 | 'Content-Type': 'application/xml'}, 198 | body='', timeout=None) 199 | send_mock_call = client.send_request.call_args_list[0][1] 200 | assert send_mock_call['http_method'] == 'PUT' 201 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events/53?unlocked=1' 202 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 203 | 204 | 205 | def test_delete_event_should_call_send_request_as_expect(): 206 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 207 | 208 | client.generate_headers = mock.Mock() 209 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 210 | 211 | client.send_request = mock.Mock() 212 | client.send_request.return_value = mock_response(status=200) 213 | 214 | event_id = '999' 215 | client.delete_event(event_id) 216 | client.send_request.assert_called_once_with( 217 | http_method='DELETE', 218 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}', 219 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 220 | 221 | 222 | def test_cancel_event_should_call_send_request_as_expected(): 223 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 224 | 225 | client.generate_headers = mock.Mock() 226 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 227 | 228 | client.send_request = mock.Mock() 229 | client.send_request.return_value = mock_response(status=200) 230 | 231 | event_id = '999' 232 | client.cancel_event(event_id) 233 | client.send_request.assert_called_once_with( 234 | http_method='POST', 235 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}/cancel', 236 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 237 | 238 | 239 | def test_start_event_should_call_send_request_as_expect(): 240 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 241 | 242 | client.generate_headers = mock.Mock() 243 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 244 | 245 | client.send_request = mock.Mock() 246 | 247 | client.send_request.return_value = mock_response(status=200) 248 | 249 | event_id = '999' 250 | client.start_event(event_id) 251 | client.send_request.assert_called_once_with( 252 | http_method='POST', 253 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}/start', 254 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, body="", timeout=None) 255 | 256 | 257 | def test_reset_event_should_call_send_request_as_expect(): 258 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 259 | 260 | client.generate_headers = mock.Mock() 261 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 262 | 263 | client.send_request = mock.Mock() 264 | 265 | client.send_request.return_value = mock_response(status=200) 266 | 267 | event_id = '999' 268 | client.reset_event(event_id) 269 | client.send_request.assert_called_once_with( 270 | http_method='POST', 271 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}/reset', 272 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, body='', timeout=None) 273 | 274 | 275 | def test_stop_event_should_call_send_request_as_expect(): 276 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 277 | 278 | client.generate_headers = mock.Mock() 279 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 280 | 281 | client.send_request = mock.Mock() 282 | client.send_request.return_value = mock_response(status=200) 283 | 284 | event_id = '999' 285 | client.stop_event(event_id) 286 | client.send_request.assert_called_once_with( 287 | http_method='POST', 288 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}/stop', 289 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, body="", timeout=None) 290 | 291 | 292 | def send_request_side_effect(**kwargs): 293 | if kwargs['url'] == f'{ELEMENTAL_ADDRESS}/live_events': 294 | return mock_response(status=200, 295 | text=file_fixture('sample_event_list.xml')) 296 | else: 297 | return mock_response(status=200, 298 | text=file_fixture('sample_device_list.xml')) 299 | 300 | 301 | def test_find_devices_in_use_will_call_send_request_as_expect(): 302 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 303 | 304 | client.generate_headers = mock.Mock() 305 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 306 | 307 | client.send_request = mock.Mock() 308 | client.send_request.return_value = \ 309 | mock_response(status=200, 310 | text=file_fixture('sample_event_list.xml')) 311 | 312 | client.find_devices_in_use() 313 | 314 | client.send_request.assert_called_with(http_method="GET", 315 | url=f'{ELEMENTAL_ADDRESS}' 316 | f'/live_events?' 317 | f'filter=active', 318 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 319 | 320 | 321 | def test_find_devices_in_use_will_return_in_used_devices(): 322 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 323 | 324 | client.generate_headers = mock.Mock() 325 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 326 | 327 | client.send_request = mock.Mock() 328 | client.send_request.return_value = \ 329 | mock_response(status=200, 330 | text=file_fixture('sample_event_list.xml')) 331 | 332 | devices = client.find_devices_in_use() 333 | assert devices == {'HD-SDI 1'} 334 | 335 | 336 | def test_get_input_devices_will_call_send_request_as_expect(): 337 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 338 | 339 | client.generate_headers = mock.Mock() 340 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 341 | 342 | client.send_request = mock.Mock() 343 | client.find_devices_in_use = mock.Mock() 344 | client.find_devices_in_use.return_value = ("HD-SDI 1",) 345 | client.send_request.return_value = \ 346 | mock_response(status=200, 347 | text=file_fixture('sample_device_list.xml')) 348 | 349 | client.get_input_devices() 350 | 351 | client.send_request.\ 352 | assert_called_with(http_method="GET", 353 | url=f'{ELEMENTAL_ADDRESS}/devices', 354 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 355 | 356 | 357 | def test_get_input_devices_will_get_right_devices_info(): 358 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 359 | 360 | client.generate_headers = mock.Mock() 361 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 362 | 363 | client.send_request = mock.Mock() 364 | client.find_devices_in_use = mock.Mock() 365 | client.find_devices_in_use.return_value = ("HD-SDI 1",) 366 | client.send_request.return_value = \ 367 | mock_response(status=200, 368 | text=file_fixture('sample_device_list.xml')) 369 | 370 | res = client.get_input_devices() 371 | assert res == [{"id": "1", 372 | "name": None, "device_name": "HD-SDI 1", 373 | "device_number": "0", "device_type": "AJA", 374 | "description": "AJA Capture Card", 375 | "channel": "1", "channel_type": "HD-SDI", 376 | "quad": "false", "availability": False}, 377 | {"id": "2", 378 | "name": None, "device_name": "HD-SDI 2", 379 | "device_number": "0", "device_type": "AJA", 380 | "description": "AJA Capture Card", 381 | "channel": "2", "channel_type": "HD-SDI", 382 | "quad": "false", "availability": True}] 383 | 384 | 385 | def test_get_input_device_by_id_will_call_send_request_as_expect(): 386 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 387 | 388 | client.generate_headers = mock.Mock() 389 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 390 | 391 | client.send_request = mock.Mock() 392 | client.find_devices_in_use = mock.Mock() 393 | client.find_devices_in_use.return_value = ("HD-SDI 1",) 394 | client.send_request.return_value = \ 395 | mock_response(status=200, 396 | text=file_fixture('sample_single_device.xml')) 397 | 398 | client.get_input_device_by_id('2') 399 | 400 | client.send_request.\ 401 | assert_called_with(http_method="GET", 402 | url=f'{ELEMENTAL_ADDRESS}/devices/2', 403 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 404 | 405 | 406 | def test_get_input_device_by_id_will_get_right_devices_info(): 407 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 408 | 409 | client.generate_headers = mock.Mock() 410 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 411 | 412 | client.send_request = mock.Mock() 413 | client.find_devices_in_use = mock.Mock() 414 | client.find_devices_in_use.return_value = ("HD-SDI 1",) 415 | client.send_request.return_value = \ 416 | mock_response(status=200, 417 | text=file_fixture('sample_single_device.xml')) 418 | 419 | res = client.get_input_device_by_id('2') 420 | assert res == {"id": "2", 421 | "name": None, "device_name": "HD-SDI 2", 422 | "device_number": "0", "device_type": "AJA", 423 | "description": "AJA Capture Card", 424 | "channel": "2", "channel_type": "HD-SDI", 425 | "quad": "false", 'availability': True} 426 | 427 | 428 | def test_get_preview_will_parse_response_json_as_expect(): 429 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 430 | 431 | client.generate_headers = mock.Mock() 432 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 433 | 434 | client.send_request = mock.Mock() 435 | client.send_request.return_value = mock_response( 436 | status=200, text=file_fixture( 437 | 'success_response_for_generate_preview.json')) 438 | 439 | response = client.generate_preview('2') 440 | 441 | assert response == { 442 | 'preview_url': f'{ELEMENTAL_ADDRESS}/' 443 | f'images/thumbs/p_1563568669_job_0.jpg'} 444 | 445 | 446 | def test_get_preview_will_raise_ElementalException_if_preview_unavailable(): 447 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 448 | 449 | client.generate_headers = mock.Mock() 450 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 451 | 452 | client.send_request = mock.Mock() 453 | client.send_request.return_value = mock_response( 454 | status=200, text=json.dumps({"type": "error", 455 | "message": "Input is invalid. " 456 | "Device already in use."})) 457 | 458 | with pytest.raises(ElementalException) as exc_info: 459 | client.generate_preview('1') 460 | 461 | respond_text = json.dumps({'type': 'error', 462 | 'message': 'Input is invalid. ' 463 | 'Device already in use.'}) 464 | assert str(exc_info.value).endswith( 465 | f"Response: 200\n" 466 | f"{respond_text}") 467 | 468 | 469 | def test_describe_event_will_call_send_request_as_expect(): 470 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 471 | 472 | client.generate_headers = mock.Mock() 473 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 474 | 475 | client.send_request = mock.Mock() 476 | response_from_elemental_api = file_fixture('sample_event.xml') 477 | client.send_request.return_value = mock_response( 478 | status=200, text=response_from_elemental_api) 479 | 480 | event_id = '999' 481 | client.describe_event(event_id) 482 | client.send_request.assert_called_once_with( 483 | http_method='GET', 484 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}', 485 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 486 | 487 | 488 | def test_describe_event_will_return_event_info_as_expect(): 489 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 490 | client.generate_headers = mock.Mock() 491 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 492 | client.send_request = mock.Mock() 493 | response_from_elemental_api = file_fixture('sample_event.xml') 494 | client.send_request.return_value = mock_response( 495 | status=200, text=response_from_elemental_api) 496 | 497 | event_id = '139' 498 | event_info = client.describe_event(event_id) 499 | assert event_info == {'origin_url': 500 | 'https://vmjhch43nfkghi.data.mediastore.us-east-1.' 501 | 'amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8', 502 | 'backup_url': 503 | 'https://vmjhch43nfkghi.data.mediastore.us-east-1.' 504 | 'amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8', 505 | 'status': 'complete'} 506 | 507 | 508 | def test_describe_event_will_return_event_info_with_empty_origin_url_if_destination_is_missing_in_response(): 509 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 510 | client.generate_headers = mock.Mock() 511 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 512 | client.send_request = mock.Mock() 513 | client.send_request.return_value = mock_response( 514 | status=200, text='') 515 | 516 | event_id = '139' 517 | event_info = client.describe_event(event_id) 518 | assert event_info == {'origin_url': '', 519 | 'backup_url': None, 520 | 'status': 'unknown'} 521 | 522 | 523 | def test_get_event_status(): 524 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 525 | 526 | client.generate_headers = mock.Mock() 527 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 528 | 529 | client.send_request = mock.Mock() 530 | response_from_elemental_api = """ 531 | 532 | ctcsdprdel5 533 | 534 | 2020-11-02 18:38:27 -0500 535 | 50 536 | false 537 | pending 538 | 0 539 | 540 | 0 541 | 00:00:00 542 | 543 | 544 | 545 | 546 | """ 547 | client.send_request.return_value = mock_response( 548 | status=200, text=response_from_elemental_api) 549 | event_id = '999' 550 | 551 | status = client.get_event_status(event_id) 552 | 553 | assert status == 'pending' 554 | client.send_request.assert_called_once_with( 555 | http_method='GET', 556 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}/status', 557 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 558 | 559 | 560 | def test_get_event_xml(): 561 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 562 | client.generate_headers = mock.Mock() 563 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 564 | 565 | client.send_request = mock.Mock() 566 | response_from_elemental_api = """ 567 | 568 | ctcsdprdel5 569 | 570 | """ 571 | client.send_request.return_value = mock_response( 572 | status=200, text=response_from_elemental_api) 573 | event_id = '18' 574 | 575 | event_xml = client.get_event_xml(event_id) 576 | 577 | assert event_xml == response_from_elemental_api 578 | client.send_request.assert_called_once_with( 579 | http_method='GET', 580 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}', 581 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 582 | 583 | 584 | def test_get_event_status_missing_status_in_elemental_response(): 585 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 586 | 587 | client.generate_headers = mock.Mock() 588 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 589 | 590 | client.send_request = mock.Mock() 591 | response_from_elemental_api = """ 592 | 593 | 594 | """ 595 | client.send_request.return_value = mock_response( 596 | status=200, text=response_from_elemental_api) 597 | event_id = '999' 598 | 599 | status = client.get_event_status(event_id) 600 | 601 | assert status == 'unknown' 602 | client.send_request.assert_called_once_with( 603 | http_method='GET', 604 | url=f'{ELEMENTAL_ADDRESS}/live_events/{event_id}/status', 605 | headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, timeout=None) 606 | 607 | 608 | @pytest.mark.parametrize('status,expected_result', [ 609 | ('pending', False), 610 | ('running', False), 611 | ('preprocessing', False), 612 | ('postprocessing', False), 613 | ('error', True), 614 | ('completed', True), 615 | ]) 616 | def test_event_can_delete(status, expected_result): 617 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 618 | 619 | client.describe_event = mock.Mock() 620 | client.describe_event.return_value = { 621 | 'status': 'pending', 622 | 'origin_url': 'fake_origin', 623 | 'backup_url': 'fake_backup' 624 | } 625 | 626 | assert client.event_can_delete('123') is False 627 | 628 | 629 | def test_event_pause_output(): 630 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 631 | client.generate_headers = mock.Mock() 632 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 633 | client.send_request = mock.Mock() 634 | client.send_request.return_value = mock_response(status=200) 635 | 636 | client.event_pause_output(event_id='53', output_id='13') 637 | 638 | client.send_request.assert_called_once_with( 639 | http_method='POST', 640 | url='FAKE_ADDRESS.com/live_events/53/pause_output', 641 | headers={ 642 | 'Accept': 'application/xml', 643 | 'Content-Type': 'application/xml' 644 | }, 645 | body='13', 646 | timeout=None 647 | ) 648 | send_mock_call = client.send_request.call_args_list[0][1] 649 | assert send_mock_call['http_method'] == 'POST' 650 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events/53/pause_output' 651 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 652 | 653 | 654 | def test_event_unpause_output(): 655 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 656 | client.generate_headers = mock.Mock() 657 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 658 | client.send_request = mock.Mock() 659 | client.send_request.return_value = mock_response(status=200) 660 | 661 | client.event_unpause_output(event_id='53', output_id='13') 662 | 663 | client.send_request.assert_called_once_with( 664 | http_method='POST', 665 | url='FAKE_ADDRESS.com/live_events/53/unpause_output', 666 | headers={ 667 | 'Accept': 'application/xml', 668 | 'Content-Type': 'application/xml' 669 | }, 670 | body='13', 671 | timeout=None 672 | ) 673 | send_mock_call = client.send_request.call_args_list[0][1] 674 | assert send_mock_call['http_method'] == 'POST' 675 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events/53/unpause_output' 676 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 677 | 678 | 679 | def test_event_start_output(): 680 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 681 | client.generate_headers = mock.Mock() 682 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 683 | client.send_request = mock.Mock() 684 | client.send_request.return_value = mock_response(status=200) 685 | 686 | client.event_start_output(event_id='53', output_id='13') 687 | 688 | client.send_request.assert_called_once_with( 689 | http_method='POST', 690 | url='FAKE_ADDRESS.com/live_events/53/start_output', 691 | headers={ 692 | 'Accept': 'application/xml', 693 | 'Content-Type': 'application/xml' 694 | }, 695 | body='13', 696 | timeout=None 697 | ) 698 | send_mock_call = client.send_request.call_args_list[0][1] 699 | assert send_mock_call['http_method'] == 'POST' 700 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events/53/start_output' 701 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 702 | 703 | 704 | def test_event_stop_output(): 705 | client = ElementalLive(ELEMENTAL_ADDRESS, USER, API_KEY) 706 | client.generate_headers = mock.Mock() 707 | client.generate_headers.return_value = {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 708 | client.send_request = mock.Mock() 709 | client.send_request.return_value = mock_response(status=200) 710 | 711 | client.event_stop_output(event_id='53', output_id='13') 712 | 713 | client.send_request.assert_called_once_with( 714 | http_method='POST', 715 | url='FAKE_ADDRESS.com/live_events/53/stop_output', 716 | headers={ 717 | 'Accept': 'application/xml', 718 | 'Content-Type': 'application/xml' 719 | }, 720 | body='13', 721 | timeout=None 722 | ) 723 | send_mock_call = client.send_request.call_args_list[0][1] 724 | assert send_mock_call['http_method'] == 'POST' 725 | assert send_mock_call['url'] == f'{ELEMENTAL_ADDRESS}/live_events/53/stop_output' 726 | assert send_mock_call['headers'] == {'Accept': 'application/xml', 'Content-Type': 'application/xml'} 727 | -------------------------------------------------------------------------------- /tests/fixtures/fail_to_create_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inputs input settings device can't be blank 4 | -------------------------------------------------------------------------------- /tests/fixtures/fail_to_start_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Invalid command 4 | Live Event 101 is not deletable! 5 | -------------------------------------------------------------------------------- /tests/fixtures/fail_to_stop_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Couldn't find LiveEvent with id=104 4 | -------------------------------------------------------------------------------- /tests/fixtures/sample_device_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 6 | HD-SDI 1 7 | 0 8 | AJA 9 | AJA Capture Card 10 | 1 11 | HD-SDI 12 | false 13 | 14 | 15 | 2 16 | 17 | HD-SDI 2 18 | 0 19 | AJA 20 | AJA Capture Card 21 | 2 22 | HD-SDI 23 | false 24 | 25 | -------------------------------------------------------------------------------- /tests/fixtures/sample_event.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 139 4 | morty demo channel 5 | 6 | false 7 | true 8 | false 9 | 10 | immediately 11 | Auto 12 | 1 13 | false 14 | 199 15 | 16 | false 17 | false 18 | 1 19 | false 20 | 21 | 22 | pending 23 | embedded 24 | 25 | 304 26 | AJA 27 | 0 28 | 2 29 | HD-SDI 30 | HD-SDI 2 31 | 32 | 2 33 | 34 | 305 35 | Auto 36 | 0 37 | 38 | 39 | 40 | follow 41 | 42 | 199 43 | 1 44 | 45 | 46 | true 47 | 199 48 | 1 49 | 1 50 | 51 | true 52 | 53 | 54 | 184 55 | 1 56 | Embedded 57 | 58 | false 59 | 184 60 | 1 61 | 1 62 | false 63 | 64 | 65 | 66 | 67 | 199 68 | sdi://0.2 69 | 70 | 71 | 72 | 0 73 | RAWVIDEO 74 | 1920 75 | 1080 76 | 29.970 77 | 1:1 78 | 79 | 80 | 81 | 82 | 1 83 | PCM_S32LE 84 | 2 85 | 2 86 | 48000 87 | 88 | 89 | 2 90 | PCM_S32LE 91 | 2 92 | 2 93 | 48000 94 | 95 | 96 | 3 97 | PCM_S32LE 98 | 2 99 | 2 100 | 48000 101 | 102 | 103 | 4 104 | PCM_S32LE 105 | 2 106 | 2 107 | 48000 108 | 109 | 110 | 5 111 | PCM_S32LE 112 | 2 113 | 2 114 | 48000 115 | 116 | 117 | 6 118 | PCM_S32LE 119 | 2 120 | 2 121 | 48000 122 | 123 | 124 | 7 125 | PCM_S32LE 126 | 2 127 | 2 128 | 48000 129 | 130 | 131 | 8 132 | PCM_S32LE 133 | 2 134 | 2 135 | 48000 136 | 137 | 138 | 139 | 140 | 9 141 | SCTE-35 142 | 143 | 144 | 10 145 | DVB-Teletext 146 | 147 | 148 | 11 149 | KLV (asynchronous) 150 | 151 | 152 | 153 | 154 | 0 155 | false 156 | 157 | 149 158 | false 159 | systemclock 160 | 161 | 162 | 1 163 | elemental5 164 | 165 | 182 166 | 50 167 | false 168 | 169 | 170 | fll_dev_1 171 | 172 | 87 173 | 0 174 | 175 | 499 176 | http://10.25.68.173:8080/signal 177 | 178 | 179 | 180 | 10000 181 | 182 182 | 000000 183 | color 184 | 1000 185 | 186 | esam 187 | 0 188 | false 189 | false 190 | false 191 | 0 192 | true 193 | true 194 | 195 | switch_input 196 | input_clock 197 | 60 198 | none 199 | false 200 | false 201 | 2019-07-31 15:04:45 -0400 202 | complete 203 | 34.949999999999996 204 | 0 205 | 0 206 | 0 207 | 2019-07-31 15:05:55 -0400 208 | 2019-07-31 15:07:32 -0400 209 | 79 210 | 211 | 212 | 11 213 | 2019-07-31T15:07:16-04:00 214 | Final timecode: 19:07:15;14 215 | 216 | 217 | 10 218 | 2019-07-31T15:05:58-04:00 219 | Initial timecode: 19:05:58;17 220 | 221 | 222 | 42 223 | 2019-07-31T15:05:58-04:00 224 | [Profiles and Levels] {"stream_assembly_0":{"profile":"high","level":"4"},"stream_assembly_1":{"profile":"main","level":"1.3"},"stream_assembly_2":{"profile":"main","level":"2.1"},"stream_assembly_3":{"profile":"main","level":"3"},"stream_assembly_4":{"profile":"high","level":"4"},"stream_assembly_5":{"profile":"high","level":"4"}} 225 | 226 | 227 | high 228 | 4 229 | 230 | 231 | main 232 | 1.3 233 | 234 | 235 | main 236 | 2.1 237 | 238 | 239 | main 240 | 3 241 | 242 | 243 | high 244 | 4 245 | 246 | 247 | high 248 | 4 249 | 250 | 251 | 252 | 253 | 254 | 884 255 | 256 | stream_assembly_0 257 | 258 | None 259 | true 260 | true 261 | gpu 262 | 263 | false 264 | 540 265 | 1035 266 | true 267 | None 268 | 100 269 | false 270 | true 271 | false 272 | 960 273 | 274 | medium 275 | 276 | 277 | 3600000 278 | true 279 | true 280 | true 281 | false 282 | 1001 283 | false 284 | 30000 285 | true 286 | 1 287 | false 288 | 3 289 | 3.0 290 | seconds 291 | 959 292 | false 293 | high 294 | 1800000 295 | 296 | 297 | 0 298 | 0 299 | 300 | 301 | 3 302 | 303 | true 304 | 305 | 1 306 | false 307 | 308 | 7 309 | false 310 | false 311 | transition_detection 312 | false 313 | 1 314 | false 315 | 316 | true 317 | 0 318 | None 319 | true 320 | 4 321 | High 322 | QVBR 323 | progressive 324 | 325 | 0 326 | 327 | h.264 328 | 329 | 330 | interpolate 331 | Deinterlace 332 | false 333 | 1025 334 | 335 | 336 | 0 337 | Conserve 338 | 310 339 | 340 | 341 | 2 342 | 343 | 344 | 16 345 | 543 346 | top_left 347 | fll - dev1 - 348 | 349 | 350 | 351 | 352 | 0 353 | true 354 | true 355 | 1041 356 | 357 | 358 | 1 359 | 360 | false 361 | 362 | false 363 | 96000 364 | 2_0 365 | 1072 366 | false 367 | false 368 | 48000 369 | LC 370 | CBR 371 | 372 | aac 373 | Audio Selector 1 374 | 375 | 376 | Embedded 377 | 730 378 | false 379 | 380 | 381 | 1 382 | Caption Selector 1 383 | 384 | 385 | 386 | 885 387 | 388 | stream_assembly_1 389 | 390 | None 391 | true 392 | true 393 | cpu_small_frames 394 | 395 | false 396 | 224 397 | 1036 398 | true 399 | None 400 | 100 401 | false 402 | true 403 | false 404 | 400 405 | 406 | medium 407 | 408 | 409 | 500000 410 | true 411 | true 412 | true 413 | false 414 | 1001 415 | false 416 | 30000 417 | true 418 | 1 419 | false 420 | 3 421 | 3.0 422 | seconds 423 | 960 424 | false 425 | high 426 | 250000 427 | 428 | 429 | 0 430 | 0 431 | 432 | 433 | 3 434 | 435 | true 436 | 437 | 1 438 | false 439 | 440 | 7 441 | false 442 | false 443 | transition_detection 444 | false 445 | 1 446 | false 447 | 448 | true 449 | 0 450 | None 451 | true 452 | Main 453 | QVBR 454 | progressive 455 | 456 | 0 457 | 458 | h.264 459 | 460 | 461 | interpolate 462 | Deinterlace 463 | false 464 | 1026 465 | 466 | 467 | 16 468 | 544 469 | top_left 470 | fll - dev1 - 471 | 472 | 473 | 474 | 475 | 0 476 | true 477 | true 478 | 1042 479 | 480 | 481 | 1 482 | 483 | false 484 | 485 | false 486 | 96000 487 | 2_0 488 | 1073 489 | false 490 | false 491 | 48000 492 | LC 493 | CBR 494 | 495 | aac 496 | Audio Selector 1 497 | 498 | 499 | Embedded 500 | 731 501 | false 502 | 503 | 504 | 1 505 | Caption Selector 1 506 | 507 | 508 | 509 | 886 510 | 511 | stream_assembly_2 512 | 513 | None 514 | true 515 | true 516 | cpu_small_frames 517 | 518 | false 519 | 288 520 | 1037 521 | true 522 | None 523 | 100 524 | false 525 | true 526 | false 527 | 512 528 | 529 | medium 530 | 531 | 532 | 1000000 533 | true 534 | true 535 | true 536 | false 537 | 1001 538 | false 539 | 30000 540 | true 541 | 1 542 | false 543 | 3 544 | 3.0 545 | seconds 546 | 961 547 | false 548 | high 549 | 500000 550 | 551 | 552 | 0 553 | 0 554 | 555 | 556 | 3 557 | 558 | true 559 | 560 | 1 561 | false 562 | 563 | 7 564 | false 565 | false 566 | transition_detection 567 | false 568 | 1 569 | false 570 | 571 | true 572 | 0 573 | None 574 | true 575 | Main 576 | QVBR 577 | progressive 578 | 579 | 0 580 | 581 | h.264 582 | 583 | 584 | interpolate 585 | Deinterlace 586 | false 587 | 1027 588 | 589 | 590 | 16 591 | 545 592 | top_left 593 | fll - dev1 - 594 | 595 | 596 | 597 | 598 | 0 599 | true 600 | true 601 | 1043 602 | 603 | 604 | 1 605 | 606 | false 607 | 608 | false 609 | 96000 610 | 2_0 611 | 1074 612 | false 613 | false 614 | 48000 615 | LC 616 | CBR 617 | 618 | aac 619 | Audio Selector 1 620 | 621 | 622 | Embedded 623 | 732 624 | false 625 | 626 | 627 | 1 628 | Caption Selector 1 629 | 630 | 631 | 632 | 887 633 | 634 | stream_assembly_3 635 | 636 | None 637 | true 638 | true 639 | cpu_small_frames 640 | 641 | false 642 | 360 643 | 1038 644 | true 645 | None 646 | 100 647 | false 648 | true 649 | false 650 | 640 651 | 652 | medium 653 | 654 | 655 | 1800000 656 | true 657 | true 658 | true 659 | false 660 | 1001 661 | false 662 | 30000 663 | true 664 | 1 665 | false 666 | 3 667 | 3.0 668 | seconds 669 | 962 670 | false 671 | high 672 | 900000 673 | 674 | 675 | 0 676 | 0 677 | 678 | 679 | 3 680 | 681 | true 682 | 683 | 1 684 | false 685 | 686 | 7 687 | false 688 | false 689 | transition_detection 690 | false 691 | 1 692 | false 693 | 694 | true 695 | 0 696 | None 697 | true 698 | Main 699 | QVBR 700 | progressive 701 | 702 | 0 703 | 704 | h.264 705 | 706 | 707 | interpolate 708 | Deinterlace 709 | false 710 | 1028 711 | 712 | 713 | 16 714 | 546 715 | top_left 716 | fll - dev1 - 717 | 718 | 719 | 720 | 721 | 0 722 | true 723 | true 724 | 1044 725 | 726 | 727 | 1 728 | 729 | false 730 | 731 | false 732 | 96000 733 | 2_0 734 | 1075 735 | false 736 | false 737 | 48000 738 | LC 739 | CBR 740 | 741 | aac 742 | Audio Selector 1 743 | 744 | 745 | Embedded 746 | 733 747 | false 748 | 749 | 750 | 1 751 | Caption Selector 1 752 | 753 | 754 | 755 | 888 756 | 757 | stream_assembly_4 758 | 759 | None 760 | true 761 | true 762 | gpu 763 | 764 | false 765 | 720 766 | 1039 767 | true 768 | None 769 | 100 770 | false 771 | true 772 | false 773 | 1280 774 | 775 | medium 776 | 777 | 778 | 6000000 779 | true 780 | true 781 | true 782 | false 783 | 1001 784 | false 785 | 30000 786 | true 787 | 1 788 | false 789 | 3 790 | 3.0 791 | seconds 792 | 963 793 | false 794 | high 795 | 3000000 796 | 797 | 798 | 0 799 | 0 800 | 801 | spatial 802 | 3 803 | 804 | true 805 | 806 | 1 807 | false 808 | 809 | 7 810 | false 811 | false 812 | transition_detection 813 | false 814 | 1 815 | false 816 | 817 | true 818 | -2 819 | None 820 | true 821 | 4 822 | High 823 | QVBR 824 | progressive 825 | 826 | 0 827 | 828 | h.264 829 | 830 | 831 | interpolate 832 | Deinterlace 833 | false 834 | 1029 835 | 836 | 837 | 0 838 | Conserve 839 | 311 840 | 841 | 842 | 2 843 | 844 | 845 | 16 846 | 547 847 | top_left 848 | fll - dev1 - 849 | 850 | 851 | 852 | 853 | 0 854 | true 855 | true 856 | 1045 857 | 858 | 859 | 1 860 | 861 | false 862 | 863 | false 864 | 96000 865 | 2_0 866 | 1076 867 | false 868 | false 869 | 48000 870 | LC 871 | CBR 872 | 873 | aac 874 | Audio Selector 1 875 | 876 | 877 | Embedded 878 | 734 879 | false 880 | 881 | 882 | 1 883 | Caption Selector 1 884 | 885 | 886 | 887 | 889 888 | 889 | stream_assembly_5 890 | 891 | None 892 | true 893 | true 894 | gpu 895 | 896 | false 897 | 720 898 | 1040 899 | true 900 | None 901 | 100 902 | false 903 | true 904 | false 905 | 1280 906 | 907 | high 908 | 909 | 910 | 12000000 911 | true 912 | true 913 | true 914 | false 915 | 1001 916 | false 917 | 60000 918 | true 919 | 1 920 | false 921 | 3 922 | 3.0 923 | seconds 924 | 964 925 | false 926 | high 927 | 6000000 928 | 929 | 930 | 0 931 | 0 932 | 933 | spatial 934 | 3 935 | 936 | true 937 | 938 | 1 939 | false 940 | 941 | 8 942 | false 943 | false 944 | transition_detection 945 | false 946 | 1 947 | false 948 | 949 | true 950 | -2 951 | None 952 | true 953 | 4 954 | High 955 | QVBR 956 | progressive 957 | 958 | 0 959 | 960 | h.264 961 | 962 | 963 | interpolate 964 | Deinterlace 965 | false 966 | 1030 967 | 968 | 969 | 0 970 | Conserve 971 | 312 972 | 973 | 974 | 2 975 | 976 | 977 | 16 978 | 548 979 | top_left 980 | fll - dev1 - 981 | 982 | 983 | 984 | 985 | 0 986 | true 987 | true 988 | 1046 989 | 990 | 991 | 1 992 | 993 | false 994 | 995 | false 996 | 96000 997 | 2_0 998 | 1077 999 | false 1000 | false 1001 | 48000 1002 | LC 1003 | CBR 1004 | 1005 | aac 1006 | Audio Selector 1 1007 | 1008 | 1009 | Embedded 1010 | 735 1011 | false 1012 | 1013 | 1014 | 1 1015 | Caption Selector 1 1016 | 1017 | 1018 | 1019 | avc_east 1020 | 406 1021 | 1022 | 1 1023 | 1024 | elemental-scte35 1025 | 1026 | 1027 | omit 1028 | AWS Elemental MediaStore 1029 | false 1030 | false 1031 | 2 1032 | false 1033 | false 1034 | 300 1035 | true 1036 | false 1037 | true 1038 | 392 1039 | true 1040 | 10 1041 | true 1042 | 11 1043 | 1044 | 1045 | false 1046 | 6 1047 | 10 1048 | emit_output 1049 | 600 1050 | 15 1051 | 1052 | 6 1053 | 10 1054 | PRIV 1055 | 10 1056 | 1057 | 1058 | false 1059 | false 1060 | false 1061 | 1062 | 500 1063 | FAKE_PASSWORD 1064 | FAKE_USERNAME 1065 | https://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8 1066 | 1067 | 1068 | apple_live_group_settings 1069 | 1070 | false 1071 | 1072 | m3u8 1073 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8_960x540_2997.m3u8 1074 | 1436 1075 | false 1076 | true 1077 | false 1078 | _960x540_2997 1079 | false 1080 | 1 1081 | 1082 | true 1083 | false 1084 | false 1085 | false 1086 | 1087 | program_audio 1088 | program_audio 1089 | 1370 1090 | false 1091 | _seg 1092 | TS 1093 | 1094 | 1095 | 4 1096 | 1417 1097 | 0 1098 | true 1099 | 1100 | 0 1101 | 1 1102 | 1103 | 482-498 1104 | 480 1105 | 500 1106 | 502 1107 | 481 1108 | 481 1109 | 1110 | stream_assembly_0 1111 | m3u8 1112 | 1113 | 1114 | false 1115 | 1116 | m3u8 1117 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8_400x224_2997.m3u8 1118 | 1437 1119 | false 1120 | false 1121 | false 1122 | _400x224_2997 1123 | false 1124 | 2 1125 | 1126 | true 1127 | false 1128 | false 1129 | false 1130 | 1131 | program_audio 1132 | program_audio 1133 | 1371 1134 | false 1135 | _seg 1136 | TS 1137 | 1138 | 1139 | 4 1140 | 1418 1141 | 0 1142 | true 1143 | 1144 | 0 1145 | 1 1146 | 1147 | 482-498 1148 | 480 1149 | 500 1150 | 481 1151 | 481 1152 | 1153 | stream_assembly_1 1154 | m3u8 1155 | 1156 | 1157 | false 1158 | 1159 | m3u8 1160 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8_512x288_2997.m3u8 1161 | 1438 1162 | false 1163 | false 1164 | false 1165 | _512x288_2997 1166 | false 1167 | 3 1168 | 1169 | true 1170 | false 1171 | false 1172 | false 1173 | 1174 | program_audio 1175 | program_audio 1176 | 1372 1177 | false 1178 | _seg 1179 | TS 1180 | 1181 | 1182 | 4 1183 | 1419 1184 | 0 1185 | true 1186 | 1187 | 0 1188 | 1 1189 | 1190 | 482-498 1191 | 480 1192 | 500 1193 | 481 1194 | 481 1195 | 1196 | stream_assembly_2 1197 | m3u8 1198 | 1199 | 1200 | false 1201 | 1202 | m3u8 1203 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8_640x360_2997.m3u8 1204 | 1439 1205 | false 1206 | false 1207 | false 1208 | _640x360_2997 1209 | false 1210 | 4 1211 | 1212 | true 1213 | false 1214 | false 1215 | false 1216 | 1217 | program_audio 1218 | program_audio 1219 | 1373 1220 | false 1221 | _seg 1222 | TS 1223 | 1224 | 1225 | 4 1226 | 1420 1227 | 0 1228 | true 1229 | 1230 | 0 1231 | 1 1232 | 1233 | 482-498 1234 | 480 1235 | 500 1236 | 481 1237 | 481 1238 | 1239 | stream_assembly_3 1240 | m3u8 1241 | 1242 | 1243 | false 1244 | 1245 | m3u8 1246 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8_1280x720_2997.m3u8 1247 | 1440 1248 | false 1249 | false 1250 | false 1251 | _1280x720_2997 1252 | false 1253 | 5 1254 | 1255 | true 1256 | false 1257 | false 1258 | false 1259 | 1260 | program_audio 1261 | program_audio 1262 | 1374 1263 | false 1264 | _seg 1265 | TS 1266 | 1267 | 1268 | 4 1269 | 1421 1270 | 0 1271 | true 1272 | 1273 | 0 1274 | 1 1275 | 1276 | 482-498 1277 | 480 1278 | 500 1279 | 481 1280 | 481 1281 | 1282 | stream_assembly_4 1283 | m3u8 1284 | 1285 | 1286 | false 1287 | 1288 | m3u8 1289 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/master/mortyg3b4.m3u8_1280x720_5994.m3u8 1290 | 1441 1291 | false 1292 | false 1293 | false 1294 | _1280x720_5994 1295 | false 1296 | 6 1297 | 1298 | true 1299 | false 1300 | false 1301 | false 1302 | 1303 | program_audio 1304 | program_audio 1305 | 1375 1306 | false 1307 | _seg 1308 | TS 1309 | 1310 | 1311 | 4 1312 | 1422 1313 | 0 1314 | true 1315 | 1316 | 0 1317 | 1 1318 | 1319 | 482-498 1320 | 480 1321 | 500 1322 | 481 1323 | 481 1324 | 1325 | stream_assembly_5 1326 | m3u8 1327 | 1328 | 1329 | 1330 | avc_west 1331 | 407 1332 | 1333 | 2 1334 | 1335 | elemental-scte35 1336 | 1337 | 1338 | omit 1339 | AWS Elemental MediaStore 1340 | false 1341 | false 1342 | 2 1343 | false 1344 | false 1345 | 300 1346 | true 1347 | false 1348 | true 1349 | 393 1350 | true 1351 | 10 1352 | true 1353 | 21 1354 | 1355 | 1356 | false 1357 | 0 1358 | 10 1359 | emit_output 1360 | 600 1361 | 15 1362 | 1363 | 6 1364 | 1365 | PRIV 1366 | 10 1367 | 1368 | 1369 | false 1370 | false 1371 | false 1372 | 1373 | 501 1374 | FAKE_PASSWORD 1375 | FAKE_USERNAME 1376 | https://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8 1377 | 1378 | 1379 | apple_live_group_settings 1380 | 1381 | false 1382 | 1383 | m3u8 1384 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8_960x540_2997.m3u8 1385 | 1442 1386 | false 1387 | true 1388 | false 1389 | _960x540_2997 1390 | false 1391 | 1 1392 | 1393 | true 1394 | false 1395 | false 1396 | false 1397 | 1398 | program_audio 1399 | program_audio 1400 | 1376 1401 | false 1402 | _seg 1403 | TS 1404 | 1405 | 1406 | 4 1407 | 1423 1408 | 0 1409 | true 1410 | 1411 | 0 1412 | 1 1413 | 1414 | 482-498 1415 | 480 1416 | 500 1417 | 502 1418 | 481 1419 | 481 1420 | 1421 | stream_assembly_0 1422 | m3u8 1423 | 1424 | 1425 | false 1426 | 1427 | m3u8 1428 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8_400x224_2997.m3u8 1429 | 1443 1430 | false 1431 | true 1432 | false 1433 | _400x224_2997 1434 | false 1435 | 2 1436 | 1437 | true 1438 | false 1439 | false 1440 | false 1441 | 1442 | program_audio 1443 | program_audio 1444 | 1377 1445 | false 1446 | _seg 1447 | TS 1448 | 1449 | 1450 | 4 1451 | 1424 1452 | 0 1453 | true 1454 | 1455 | 0 1456 | 1 1457 | 1458 | 482-498 1459 | 480 1460 | 500 1461 | 502 1462 | 481 1463 | 481 1464 | 1465 | stream_assembly_1 1466 | m3u8 1467 | 1468 | 1469 | false 1470 | 1471 | m3u8 1472 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8_512x288_2997.m3u8 1473 | 1444 1474 | false 1475 | true 1476 | false 1477 | _512x288_2997 1478 | false 1479 | 3 1480 | 1481 | true 1482 | false 1483 | false 1484 | false 1485 | 1486 | program_audio 1487 | program_audio 1488 | 1378 1489 | false 1490 | _seg 1491 | TS 1492 | 1493 | 1494 | 4 1495 | 1425 1496 | 0 1497 | true 1498 | 1499 | 0 1500 | 1 1501 | 1502 | 482-498 1503 | 480 1504 | 500 1505 | 502 1506 | 481 1507 | 481 1508 | 1509 | stream_assembly_2 1510 | m3u8 1511 | 1512 | 1513 | false 1514 | 1515 | m3u8 1516 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8_640x360_2997.m3u8 1517 | 1445 1518 | false 1519 | true 1520 | false 1521 | _640x360_2997 1522 | false 1523 | 4 1524 | 1525 | true 1526 | false 1527 | false 1528 | false 1529 | 1530 | program_audio 1531 | program_audio 1532 | 1379 1533 | false 1534 | _seg 1535 | TS 1536 | 1537 | 1538 | 4 1539 | 1426 1540 | 0 1541 | true 1542 | 1543 | 0 1544 | 1 1545 | 1546 | 482-498 1547 | 480 1548 | 500 1549 | 502 1550 | 481 1551 | 481 1552 | 1553 | stream_assembly_3 1554 | m3u8 1555 | 1556 | 1557 | false 1558 | 1559 | m3u8 1560 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8_1280x720_2997.m3u8 1561 | 1446 1562 | false 1563 | true 1564 | false 1565 | _1280x720_2997 1566 | false 1567 | 5 1568 | 1569 | true 1570 | false 1571 | false 1572 | false 1573 | 1574 | program_audio 1575 | program_audio 1576 | 1380 1577 | false 1578 | _seg 1579 | TS 1580 | 1581 | 1582 | 4 1583 | 1427 1584 | 0 1585 | true 1586 | 1587 | 0 1588 | 1 1589 | 1590 | 482-498 1591 | 480 1592 | 500 1593 | 502 1594 | 481 1595 | 481 1596 | 1597 | stream_assembly_4 1598 | m3u8 1599 | 1600 | 1601 | false 1602 | 1603 | m3u8 1604 | mediastoressl://vmjhch43nfkghi.data.mediastore.us-east-1.amazonaws.com/mortyg3b4/backup/mortyg3b4.m3u8_1280x720_5994.m3u8 1605 | 1447 1606 | false 1607 | true 1608 | false 1609 | _1280x720_5994 1610 | false 1611 | 6 1612 | 1613 | true 1614 | false 1615 | false 1616 | false 1617 | 1618 | program_audio 1619 | program_audio 1620 | 1381 1621 | false 1622 | _seg 1623 | TS 1624 | 1625 | 1626 | 4 1627 | 1428 1628 | 0 1629 | true 1630 | 1631 | 0 1632 | 1 1633 | 1634 | 482-498 1635 | 480 1636 | 500 1637 | 502 1638 | 481 1639 | 481 1640 | 1641 | stream_assembly_5 1642 | m3u8 1643 | 1644 | 1645 | 1646 | -------------------------------------------------------------------------------- /tests/fixtures/sample_event_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SuperBowl AVC Live (FLL - Dev) 5 | 6 | true 7 | false 8 | 9 | immediately 10 | Auto 11 | 1 12 | false 13 | 14 | false 15 | false 16 | 1 17 | false 18 | 19 | 20 | embedded 21 | 22 | AJA 23 | 0 24 | 1 25 | HD-SDI 26 | HD-SDI 1 27 | 28 | 29 | Auto 30 | 0 31 | 32 | 33 | input_1 34 | 35 | follow 36 | 37 | 1 38 | input_1_video_selector_0 39 | 40 | 41 | true 42 | 1 43 | 1 44 | 45 | true 46 | input_1_audio_selector_0 47 | 48 | 49 | 1 50 | Embedded 51 | 52 | false 53 | 1 54 | 1 55 | false 56 | 57 | 58 | 59 | false 60 | 61 | false 62 | systemclock 63 | 64 | 65 | 1 66 | 67 | 50 68 | false 69 | 70 | 71 | fll_dev_1 72 | 73 | 0 74 | 75 | http://10.25.68.173:8080/signal 76 | 77 | 78 | 79 | 10000 80 | 000000 81 | color 82 | 1000 83 | 84 | esam 85 | 0 86 | false 87 | false 88 | false 89 | 0 90 | true 91 | true 92 | 93 | switch_input 94 | input_clock 95 | 60 96 | none 97 | false 98 | false 99 | 2019-07-10 14:06:06 -0400 100 | running 101 | 0 102 | 103 | 104 | 105 | 106 | 0 107 | 108 | 109 | -------------------------------------------------------------------------------- /tests/fixtures/sample_single_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2 4 | 5 | HD-SDI 2 6 | 0 7 | AJA 8 | AJA Capture Card 9 | 2 10 | HD-SDI 11 | false 12 | -------------------------------------------------------------------------------- /tests/fixtures/success_response_for_create.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 53 4 | new-event 5 | 6 | false 7 | true 8 | false 9 | 10 | immediately 11 | Auto 12 | 1 13 | false 14 | 59 15 | 16 | false 17 | false 18 | 1 19 | false 20 | 21 | 22 | pending 23 | embedded 24 | 25 | 50 26 | AJA 27 | 0 28 | 2 29 | HD-SDI 30 | HD-SDI 2 31 | IPENC12 - Mirror 32 | 2 33 | 34 | 50 35 | Auto 36 | 0 37 | 38 | 39 | 40 | follow 41 | 42 | 59 43 | 1 44 | 45 | 46 | true 47 | 59 48 | 1 49 | 1 50 | 51 | true 52 | 53 | 54 | 32 55 | 1 56 | Embedded 57 | 58 | true 59 | 32 60 | 1 61 | 1 62 | true 63 | 64 | 65 | 66 | Waiting for device input... 67 | 68 | 69 | false 70 | 71 | 63 72 | true 73 | embedded 74 | 75 | 76 | 1 77 | nybc-ss-prod1 78 | 79 | 83 80 | 50 81 | false 82 | 83 | 84 | 10000 85 | 83 86 | 000000 87 | color 88 | 1000 89 | 90 | scte35_splice_insert 91 | 0 92 | false 93 | false 94 | false 95 | 0 96 | true 97 | true 98 | 99 | switch_input 100 | input_clock 101 | 60 102 | none 103 | false 104 | false 105 | 2020-10-02 14:57:30 -0400 106 | pending 107 | 0 108 | 109 | 110 | 111 | 112 | 0 113 | 114 | 201 115 | 116 | stream_assembly_0 117 | 118 | None 119 | true 120 | 121 | 122 | false 123 | 1080 124 | 348 125 | true 126 | None 127 | 50 128 | false 129 | true 130 | false 131 | 1920 132 | 133 | high 134 | default 135 | false 136 | 20000000 137 | 138 | 40000000 139 | false 140 | true 141 | 1001 142 | false 143 | 30000 144 | false 145 | 1 146 | 1 147 | 2.0 148 | seconds 149 | 64 150 | false 151 | high 152 | 153 | 154 | 155 | 0 156 | 157 | 1 158 | 159 | true 160 | 161 | 1 162 | 163 | 164 | transition_detection 165 | false 166 | 0 167 | false 168 | true 169 | 0.0 170 | None 171 | true 172 | false 173 | false 174 | Main/High 175 | CBR 176 | progressive 177 | 178 | 179 | 180 | h.265 181 | 182 | 183 | interpolate 184 | Deinterlace 185 | false 186 | 427 187 | 188 | 189 | 190 | 191 | 0 192 | true 193 | true 194 | 390 195 | 196 | 197 | 1 198 | 199 | false 200 | 201 | false 202 | 384000 203 | 2_0 204 | 468 205 | false 206 | false 207 | 48000 208 | LC 209 | CBR 210 | 211 | aac 212 | Audio Selector 1 213 | 214 | 215 | 216 | Embedded 217 | false 218 | 35 219 | false 220 | ENG 221 | ENGLISH 222 | 1 223 | Caption Selector 1 224 | 225 | 226 | 227 | udp_ts 228 | 84 229 | 230 | 1 231 | 232 | 50 233 | drop_ts 234 | PRIV 235 | 10 236 | 237 | udp_group_settings 238 | 239 | false 240 | 241 | false 242 | m2ts 243 | 206 244 | false 245 | false 246 | false 247 | false 248 | false 249 | false 250 | 251 | false 252 | 1 253 | 254 | false 255 | false 256 | false 257 | false 258 | 259 | 1000 260 | 50 261 | 7 262 | none 263 | 264 | 5 265 | 50 266 | true 267 | true 268 | 5 269 | 270 | 271 | 132 272 | rtp://54.158.42.171:23232 273 | 274 | 275 | 133 276 | rtp://54.158.42.171:23233 277 | 278 | 279 | 280 | false 281 | 2 282 | 0 283 | false 284 | false 285 | false 286 | true 287 | false 288 | false 289 | 209 290 | false 291 | 292 | 100 293 | true 294 | 295 | 100 296 | 1 297 | 0 298 | none 299 | maintain_cadence 300 | 301 | 302 | false 303 | true 304 | false 305 | NO_ENCRYPTION 306 | 307 | 6000 308 | 309 | 310 | 50 311 | sdt_follow 312 | 500 313 | 314 | 315 | 316 | 482-498 317 | 460-479 318 | 450-459 319 | 499 320 | 480 321 | 481 322 | 481 323 | 324 | stream_assembly_0 325 | m2ts 326 | 327 | 328 | 329 | -------------------------------------------------------------------------------- /tests/fixtures/success_response_for_generate_preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "audio_level": 0, 3 | "last_stats": { 4 | "type": "job", 5 | "fill_msec": "0", 6 | "id": "0", 7 | "event_state": "running", 8 | "elapsed": 1, 9 | "media_0": { 10 | "id": "0", 11 | "state": "running", 12 | "frames_complete": "0", 13 | "uri": "/dev/null", 14 | "fps": "0.0", 15 | "pct_rt": "0", 16 | "psnr": "0.0", 17 | "audio_level": "0", 18 | "elapsed": 1, 19 | "pct": "Streaming" 20 | }, 21 | "svq": { 22 | "quality1": "0.000", 23 | "quality2": "0.000", 24 | "quality3": "0.000" 25 | }, 26 | "active_input": "0", 27 | "buffer_avg": "0", 28 | "buffer_max": "0", 29 | "dropped_frames": "0", 30 | "inputs": [ 31 | { 32 | "id": "0", 33 | "state": "quarantined", 34 | "buffer_avg": "0", 35 | "buffer_max": "0", 36 | "dropped_frames": "0", 37 | "input_information": "SDI0002019-07-19T00:00:00ZRAWVIDEO1920x10801:14:2:229.97PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000PCM_S32LEunk2S3248000" 38 | } 39 | ] 40 | }, 41 | "demux_info": "general:\n id: '0'\n stream_uri: sdi://0.2\nvideo_streams:\n- stream_id: '0'\n codec: RAWVIDEO\n width: '1920'\n height: '1080'\n framerate: '29.970'\n SAR: '1:1'\naudio_streams:\n- stream_id: '1'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '2'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '3'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '4'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '5'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '6'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '7'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\n- stream_id: '8'\n codec: PCM_S32LE\n channels: '2'\n sample_format: '2'\n sample_rate: '48000'\ndata:\n- stream_id: '9'\n codec: SCTE-35\n- stream_id: '10'\n codec: DVB-Teletext\n- stream_id: '11'\n codec: KLV (asynchronous)", 42 | "demux_hash": { 43 | "general": { 44 | "id": "0", 45 | "stream_uri": "sdi://0.2" 46 | }, 47 | "video_streams": { 48 | "1": { 49 | "stream_id": "0", 50 | "codec": "RAWVIDEO", 51 | "width": "1920", 52 | "height": "1080", 53 | "framerate": "29.970", 54 | "SAR": "1:1" 55 | } 56 | }, 57 | "audio_streams": { 58 | "1": { 59 | "stream_id": "1", 60 | "codec": "PCM_S32LE", 61 | "channels": "2", 62 | "sample_format": "2", 63 | "sample_rate": "48000" 64 | }, 65 | "2": { 66 | "stream_id": "2", 67 | "codec": "PCM_S32LE", 68 | "channels": "2", 69 | "sample_format": "2", 70 | "sample_rate": "48000" 71 | }, 72 | "3": { 73 | "stream_id": "3", 74 | "codec": "PCM_S32LE", 75 | "channels": "2", 76 | "sample_format": "2", 77 | "sample_rate": "48000" 78 | }, 79 | "4": { 80 | "stream_id": "4", 81 | "codec": "PCM_S32LE", 82 | "channels": "2", 83 | "sample_format": "2", 84 | "sample_rate": "48000" 85 | }, 86 | "5": { 87 | "stream_id": "5", 88 | "codec": "PCM_S32LE", 89 | "channels": "2", 90 | "sample_format": "2", 91 | "sample_rate": "48000" 92 | }, 93 | "6": { 94 | "stream_id": "6", 95 | "codec": "PCM_S32LE", 96 | "channels": "2", 97 | "sample_format": "2", 98 | "sample_rate": "48000" 99 | }, 100 | "7": { 101 | "stream_id": "7", 102 | "codec": "PCM_S32LE", 103 | "channels": "2", 104 | "sample_format": "2", 105 | "sample_rate": "48000" 106 | }, 107 | "8": { 108 | "stream_id": "8", 109 | "codec": "PCM_S32LE", 110 | "channels": "2", 111 | "sample_format": "2", 112 | "sample_rate": "48000" 113 | } 114 | }, 115 | "data": { 116 | "1": { 117 | "stream_id": "9", 118 | "codec": "SCTE-35" 119 | }, 120 | "2": { 121 | "stream_id": "10", 122 | "codec": "DVB-Teletext" 123 | }, 124 | "3": { 125 | "stream_id": "11", 126 | "codec": "KLV (asynchronous)" 127 | } 128 | } 129 | }, 130 | "preview_image_id": 1563568669 131 | } -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 170 3 | ignore = E501,E306,W503,W504,E731 4 | 5 | [tox] 6 | isolated_build = True 7 | envlist = 8 | py38 9 | deadfixtures 10 | isort-check 11 | flake8 12 | 13 | [testenv] 14 | allowlist_externals = poetry 15 | basepython=python3 16 | commands = 17 | poetry install -v 18 | poetry run pytest -vv --color=yes --mypy --tb=short --doctest-modules elemental/ {posargs:--cov=elemental} tests 19 | 20 | [testenv:deadfixtures] 21 | deps = 22 | pytest-deadfixtures==2.2.1 23 | commands = poetry run pytest -vv --dead-fixtures --color=yes --tb=short elemental/ 24 | 25 | [testenv:isort-check] 26 | deps= 27 | isort==5.5.4 28 | changedir={toxinidir}/elemental 29 | commands = isort -c -df . 30 | 31 | [testenv:flake8] 32 | changedir={toxinidir}/elemental 33 | deps= 34 | flake8==3.8.4 35 | commands = flake8 36 | --------------------------------------------------------------------------------