├── .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 | [](https://github.com/cbsinteractive/elemental/actions?query=workflow%3ACI)
5 | [](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 |
1113 |
1156 |
1199 |
1242 |
1285 |
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 |
1424 |
1468 |
1512 |
1556 |
1600 |
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 |
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 |
--------------------------------------------------------------------------------