pax_global_header00006660000000000000000000000064147364306160014524gustar00rootroot0000000000000052 comment=6b52bf542697cc80b1a5b7dd26d8c07ec45d44a9 openwebifpy-4.3.1/000077500000000000000000000000001473643061600140605ustar00rootroot00000000000000openwebifpy-4.3.1/.editorconfig000066400000000000000000000004441473643061600165370ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab openwebifpy-4.3.1/.github/000077500000000000000000000000001473643061600154205ustar00rootroot00000000000000openwebifpy-4.3.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000005021473643061600201220ustar00rootroot00000000000000* openwebifpy version: * Python version: * Operating System: ### Description Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. ### What I Did ``` Paste the command(s) you ran and the output. If there was a crash, please include the traceback here. ``` openwebifpy-4.3.1/.github/workflows/000077500000000000000000000000001473643061600174555ustar00rootroot00000000000000openwebifpy-4.3.1/.github/workflows/python_publish.yml000066400000000000000000000020741473643061600232520ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} openwebifpy-4.3.1/.gitignore000066400000000000000000000022341473643061600160510ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .idea/ openwebifpy-4.3.1/CONTRIBUTING.rst000066400000000000000000000067651473643061600165370ustar00rootroot00000000000000.. highlight:: shell ============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/autinerd/openwebifpy/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ openwebifpy could always use more documentation, whether as part of the official openwebifpy docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/autinerd/openwebifpy/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up `openwebifpy` for local development. 1. Fork the `openwebifpy` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/openwebifpy.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv openwebifpy $ cd openwebifpy/ $ python setup.py develop 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ flake8 openwebifpy tests $ python setup.py test or py.test $ tox To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check https://travis-ci.org/fbradyirl/openwebifpy/pull_requests and make sure that the tests pass for all supported Python versions. Tips ---- To run a subset of tests:: $ python -m unittest tests.test_openwebifpy Deploying --------- A reminder for the maintainers on how to deploy. Make sure all your changes are committed (including an entry in HISTORY.rst). Then run:: $ bumpversion patch # possible: major / minor / patch $ git push $ git push --tags Travis will then deploy to PyPI if tests pass. openwebifpy-4.3.1/HISTORY.rst000066400000000000000000000057531473643061600157650ustar00rootroot00000000000000======= History ======= 1.0.4 (2019-03-01) ------------------ * Move to new repo. 1.0.5 (2019-03-01) ------------------ * First travis deploy 1.0.6 (2019-03-04) ------------------ * added get_bouquet_sources 1.0.7 (2019-03-04) ------------------ * Load src on startup + add select source 1.0.8 (2019-03-04) ------------------ * Fix tox 1.0.9 (2019-03-04) ------------------ * Retry deploy 1.1.0 (2019-03-04) ------------------ * Return screenshot URL if no picon 1.1.1 (2019-03-05) ------------------ * Return screenshot URL if recording playback 1.1.2 (2019-03-05) ------------------ * Fix bug 1.1.3 (2019-03-05) ------------------ * adding parse channel name from recording for picon. 1.1.4 (2019-03-05) ------------------ * Fix channel name in status info on recording playback 1.1.5 (2019-03-05) ------------------ * Adding send stop command 1.1.6 (2019-03-05) ------------------ * Debug all the logs 1.1.7 (2019-03-05) ------------------ * bug fix 1.1.8 (2019-03-05) ------------------ * Randomise the picon url so image doesnt get cached. 1.1.9 (2019-03-05) ------------------ * Docs update 1.2.0 (2019-03-06) ------------------ * adding prefer_picon parameter 1.2.1 (2019-03-06) ------------------ * Tidy up api class 1.2.2 (2019-03-06) ------------------ * Bug fix with sources 1.2.3 (2019-03-06) ------------------ * Bug fix 1.2.4 (2019-03-06) ------------------ * Bug fix 1.2.5 (2019-03-06) ------------------ * Make build faster 1.2.6 (2019-03-06) ------------------ * Default all 1.2.7 (2019-03-08) ------------------ * Check in_standby state before going into or out of standby 2.0.0 (2019-03-17) ------------------ * Fixing up tox to include pylint * Fixes for same * Adding zeroconf discovery for e2 boxes. 2.0.1 (2019-03-17) ------------------ * No change. 3.0.0 (2019-03-24) ------------------ * Adding deep standby feature. * If in deep standby, dont throw exceptions on every update() 3.0.1 (2019-03-24) ------------------ * Allow pass in source_bouquet 3.0.2 (2019-03-24) ------------------ * Fix tox 3.0.3 (2019-03-24) ------------------ * Catch connection error on deep standby 3.0.4 (2019-03-24) ------------------ * Unpin all reqs 3.0.5 (2019-03-24) ------------------ * Fix bug 3.0.6 (2019-03-24) ------------------ * Handle deep standby 3.0.7 (2019-03-24) ------------------ * Fix offline reset 3.0.8 (2019-03-24) ------------------ * Handle getversion on host down 3.0.9 (2019-03-24) ------------------ * Remove I/O from init 3.1.0 (2019-03-24) ------------------ * Add back get_version to init 3.1.1 (2019-04-08) ------------------ * Handle bad connection to fetch bouquets 3.1.2 (2020-12-08) ------------------ * Switch pipeline to Github actions 3.1.3 (2020-12-08) ------------------ * Fix selecting channel source. Works for IPTV channels now also. 3.1.6 (2020-12-09) ------------------ * Fix for issues/14 3.2.0 (2020-12-12) ------------------ * Fix for issues/12 3.2.7 (2021-01-05) ------------------ * Only use a single session per device * Log error on connection error. openwebifpy-4.3.1/LICENSE000066400000000000000000000020601473643061600150630ustar00rootroot00000000000000MIT License Copyright (c) 2019, Finbarr Brady Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. openwebifpy-4.3.1/README.md000066400000000000000000000007041473643061600153400ustar00rootroot00000000000000# openwebifpy [![openwebifpy on PyPI](https://img.shields.io/pypi/v/openwebifpy.svg)](https://pypi.python.org/pypi/openwebifpy) Provides a python interface to interact with a device running OpenWebIf - Free software: MIT license - OpenWebif API docs: https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/OpenWebif-API-documentation ## Features - Basic control of an Enigma2 box running OpenWebif. - Ability to send messages to the screen. openwebifpy-4.3.1/openwebif/000077500000000000000000000000001473643061600160365ustar00rootroot00000000000000openwebifpy-4.3.1/openwebif/__init__.py000066400000000000000000000000761473643061600201520ustar00rootroot00000000000000"""Top-level package for openwebif.""" __version__ = "4.3.1" openwebifpy-4.3.1/openwebif/api.py000066400000000000000000000453351473643061600171730ustar00rootroot00000000000000"""API for communicating with OpenWebIf.""" from __future__ import annotations import logging import unicodedata from collections import OrderedDict from dataclasses import dataclass from re import sub from time import time from typing import TYPE_CHECKING, Any, cast import aiohttp from yarl import URL from .constants import ( PATH_ABOUT, PATH_BOUQUETS, PATH_EPGNOW, PATH_GETALLSERVICES, PATH_GRAB, PATH_MESSAGE, PATH_POWERSTATE, PATH_REMOTECONTROL, PATH_STATUSINFO, PATH_VOL, PATH_ZAP, ) from .enums import ( MessageType, PlaybackType, PowerState, RemoteControlCodes, ScreenGrabFormat, ScreenGrabMode, SetVolumeOption, SType, ) from .error import InvalidAuthError if TYPE_CHECKING: from collections.abc import Mapping from openwebif.types import Bouquets _LOGGER = logging.getLogger(__name__) def enable_logging() -> None: """Set up the logging for home assistant.""" logging.basicConfig(level=logging.INFO) # pylint: disable=too-many-instance-attributes @dataclass class OpenWebIfServiceEvent: """Represent a OpenWebIf service event.""" filename: str | None = None id: int | None = None name: str | None = None serviceref: str | None = None begin: str | None = None begin_timestamp: int | None = None end: str | None = None end_timestamp: int | None = None description: str | None = None fulldescription: str | None = None station: str | None = None # pylint: disable=too-many-instance-attributes @dataclass class OpenWebIfStatus: """Repesent a OpenWebIf status.""" currservice: OpenWebIfServiceEvent volume: int | None = None muted: bool | None = None in_standby: bool | None = False is_recording: bool | None = False streaming_list: str | None = None is_streaming: bool | None = False status_info: dict | None = None is_recording_playback: bool | None = False # pylint: disable=too-many-instance-attributes, disable=too-many-public-methods class OpenWebIfDevice: """Represent a OpenWebIf client device.""" _session: aiohttp.ClientSession | None base: URL status: OpenWebIfStatus is_offline: bool = False turn_off_to_deep: bool picon_url: str | None = None source_bouquet: str | None = None mac_address: str | None = None sources: dict[str, Any] source_list: list[str] bouquet_list: OrderedDict[str, str] # {bouquetname: bouquetref} def __init__( self, host: str | aiohttp.ClientSession, port: int = 80, username: str | None = None, password: str | None = None, is_https: bool = False, turn_off_to_deep: bool = False, source_bouquet: str | None = None, ) -> None: """Define an enigma2 device. :param host: IP or hostname or a ClientSession :param port: OpenWebif port :param username: e2 user :param password: e2 user password :param is_https: use https or not :param turn_off_to_deep: If True, send to deep standby on turn off :param source_bouquet: Which bouquet ref you want to load """ enable_logging() if isinstance(host, str): _LOGGER.debug("Initialising new openwebif client for host: %s", host) self.base = URL.build( scheme="http" if not is_https else "https", host=host, port=port, user=username, password=password, ) self._session = aiohttp.ClientSession(self.base) else: self.base = cast(URL, host._base_url) # noqa: SLF001 self._session = host self.turn_off_to_deep = turn_off_to_deep self.source_bouquet = source_bouquet self.status = OpenWebIfStatus(currservice=OpenWebIfServiceEvent()) self.sources = {} self.source_list = [] self.bouquet_list = OrderedDict() async def close(self) -> None: """Close the connection.""" if self._session is not None and not self._session.closed: await self._session.close() self._session = None def default_all(self) -> None: """Set all properties to default.""" self.status = OpenWebIfStatus(currservice=OpenWebIfServiceEvent()) async def get_about(self) -> dict[str, Any]: """Get general information.""" return await self._call_api(PATH_ABOUT) async def get_status_info(self) -> dict[str, Any]: """Get status info.""" return await self._call_api(PATH_STATUSINFO) async def update(self) -> None: """Refresh current state based from /api/statusinfo.""" self.status.status_info = await self._call_api(PATH_STATUSINFO) if self.is_offline or not self.status.status_info: self.default_all() return self.status.currservice.filename = self.status.status_info.get( "currservice_filename", ) self.status.currservice.id = self.status.status_info.get("currservice_id") self.status.currservice.name = self.status.status_info.get("currservice_name") self.status.currservice.serviceref = self.status.status_info.get( "currservice_serviceref", ) self.status.currservice.begin = self.status.status_info.get("currservice_begin") self.status.currservice.begin_timestamp = self.status.status_info.get( "currservice_begin_timestamp", ) self.status.currservice.end = self.status.status_info.get("currservice_end") self.status.currservice.end_timestamp = self.status.status_info.get( "currservice_end_timestamp", ) self.status.currservice.description = self.status.status_info.get( "currservice_description", ) self.status.currservice.station = self.status.status_info.get( "currservice_station", ) self.status.currservice.fulldescription = self.status.status_info.get( "currservice_fulldescription", ) self.status.in_standby = self.status.status_info["inStandby"] == "true" self.status.is_recording = self.status.status_info["isRecording"] == "true" if "isStreaming" in self.status.status_info: self.status.is_streaming = self.status.status_info["isStreaming"] == "true" else: self.status.is_streaming = None self.status.muted = self.status.status_info.get("muted") self.status.volume = self.status.status_info.get("volume") if not self.sources: self.sources = await self.get_bouquet_sources(bouquet=self.source_bouquet) self.source_list = list(self.sources.keys()) if self.get_current_playback_type() == PlaybackType.RECORDING: # try get correct channel name channel_name = self.get_channel_name_from_serviceref() self.status.status_info["currservice_station"] = channel_name self.status.currservice.station = channel_name self.status.currservice.name = f"🔴 {self.status.currservice.name}" if not self.status.in_standby: url = await self.get_current_playing_picon_url( channel_name=self.status.currservice.station, currservice_serviceref=self.status.currservice.serviceref, ) self.picon_url = str(self.base.with_path(url)) if url is not None else None async def get_volume(self) -> int: """Get the current volume.""" return int((await self._call_api(PATH_VOL))["current"]) async def set_volume(self, new_volume: int | SetVolumeOption) -> bool: """Set the volume to the new value. :param new_volume: int from 0-100 :return: True if successful, false if there was a problem """ return self._check_response_result( await self._call_api( PATH_VOL, { "set": ("set" + str(new_volume)) if isinstance(new_volume, int) else str(new_volume), }, ), ) async def send_message( self, text: str, message_type: MessageType = MessageType.INFO, timeout: int = -1, ) -> bool: """Send a message to the TV screen. :param text: The message to display :param message_type: The type of message (0 = YES/NO, 1 = INFO, 2 = WARNING, 3 = ERROR) :return: True if successful, false if there was a problem """ return self._check_response_result( await self._call_api( PATH_MESSAGE, {"timeout": timeout, "type": message_type.value, "text": text}, ), ) async def turn_on(self) -> bool: """Take the box out of standby.""" if self.is_offline: _LOGGER.debug("Box is offline, going to try wake on lan") # self.wake_up() return self._check_response_result( await self._call_api(PATH_POWERSTATE, {"newstate": PowerState.WAKEUP}), ) def get_screen_grab_url( self, mode: ScreenGrabMode = ScreenGrabMode.ALL, file_format: ScreenGrabFormat = ScreenGrabFormat.JPG, resolution: int = 0, ) -> URL: """Get the URL for a screen grab. :param mode: The screen grab mode :param file_format: The picture format :param resolution: The resolution to grab (0 = native resolution) :return: The URL for the screen grab """ return self.base.with_path(PATH_GRAB).with_query( { "mode": mode.value, "format": file_format.value, "t": int(time()), "r": resolution, }, ) async def turn_off(self) -> bool: """Put the box out into standby.""" if self.turn_off_to_deep: return await self.deep_standby() return self._check_response_result( await self._call_api(PATH_POWERSTATE, {"newstate": PowerState.STANDBY}), ) async def deep_standby(self) -> bool: """Go into deep standby.""" return self._check_response_result( await self._call_api( PATH_POWERSTATE, {"newstate": PowerState.DEEP_STANDBY}, ), ) async def set_powerstate(self, newstate: PowerState) -> bool: """Set a new power state.""" return self._check_response_result( await self._call_api(PATH_POWERSTATE, {"newstate": newstate.value}), ) async def send_remote_control_action(self, action: RemoteControlCodes) -> bool: """Send a remote control command.""" return self._check_response_result( await self._call_api(PATH_REMOTECONTROL, {"command": action.value}), ) async def toggle_mute(self) -> bool: """Send mute command.""" response = await self._call_api(PATH_VOL, {"set": SetVolumeOption.MUTE}) return False if response is None else bool(response["ismute"]) @staticmethod def _check_response_result(response: dict[str, Any] | None) -> bool: """Check the result of the response. :param response: :return: Returns True if command success, else, False """ return False if response is None else bool(response["result"]) def is_currently_recording_playback(self) -> bool: """Return true if playing back recording.""" return self.get_current_playback_type() == PlaybackType.RECORDING def get_current_playback_type(self) -> PlaybackType | None: """Get the currservice_serviceref playing media type. :return: PlaybackType.LIVE or PlaybackType.RECORDING """ if self.status.currservice and self.status.currservice.serviceref: if self.status.currservice.serviceref.startswith("1:0:0"): # This is a recording, not a live channel return PlaybackType.RECORDING return PlaybackType.LIVE return None async def get_current_playing_picon_url( self, channel_name: str | None = None, currservice_serviceref: str | None = None, ) -> str | None: """Return the URL to the picon image for the currently playing channel. :param channel_name: If specified, it will base url on this channel name else, fetch latest from get_status_info() :param currservice_serviceref: The service_ref for the current service :return: The URL, or None if not available """ if channel_name is None: channel_name = self.status.currservice.station currservice_serviceref = str(self.status.currservice.serviceref) if self.status.is_recording_playback: channel_name = self.get_channel_name_from_serviceref() url = f"/picon/{self.get_picon_name(str(channel_name))}.png" _LOGGER.debug("trying picon url (by channel name): %s", url) if await self.url_exists(url): return url # Last ditch attempt. # Now try old way, using service ref name. # See https://github.com/home-assistant/home-assistant/issues/22293 # # e.g. # sref: "1:0:19:2887:40F:1:C00000:0:0:0:" # url: http://vusolo2/picon/1_0_19_2887_40F_1_C00000_0_0_0.png) url = f"/picon/{currservice_serviceref.strip(':').replace(':', '_')}.png" _LOGGER.debug("trying picon url (with sref): %s", url) if await self.url_exists(url): return url _LOGGER.debug("Could not find picon for: %s", channel_name) return None def get_channel_name_from_serviceref(self) -> str | None: """Try to get the channel name from the recording file name.""" if self.status.currservice.serviceref is None: return None if len(splits := self.status.currservice.serviceref.split("-")) >= 2: # We guess the channel name based on the default recording name scheme # Example file name: "20201019 2027 - SBS6 HD - Chateau Meiland" return splits[1].strip() return self.status.currservice.serviceref async def url_exists(self, url: str) -> bool: """Check if a given URL responds to a HEAD request. :param url: url to test :return: True or False """ if self._session is None: self._session = aiohttp.ClientSession(self.base) request = await self._session.head(url) if request.status == 200: return True _LOGGER.debug("url at %s does not exist.", url) return False @staticmethod def get_picon_name(channel_name: str) -> str: """Get the name as format is outlined here. https://github.com/openatv/enigma2/blob/master/lib/python/Components/Renderer/Picon.py :param channel_name: The name of the channel :return: the correctly formatted name """ _LOGGER.debug("Getting Picon URL for %s", channel_name) return sub( "[^a-z0-9]", "", ( unicodedata.normalize("NFKD", channel_name) .encode("ASCII", "ignore") .decode("utf-8") ) .replace("&", "and") .replace("+", "plus") .replace("*", "star") .lower(), ) async def get_version(self) -> str: """Return the Openwebif version.""" about = await self.get_about() return str(about["info"]["webifver"]) if about is not None else None async def get_bouquet_sources(self, bouquet: str | None = None) -> dict[str, Any]: """Get a dict of source names and sources in the bouquet. If bouquet is None, the first bouquet will be read from. :param bouquet: The bouquet :return: a dict """ sources: dict[str, Any] = {} bRef = "" if not self.bouquet_list: self.bouquet_list = OrderedDict( {b[1]: b[0] for b in (await self.get_all_bouquets())["bouquets"]}, ) if not bouquet: # load first bouquet if len(self.bouquet_list) == 0: _LOGGER.debug("%s: No bouquets were found.", self.base) return sources bRef = next(iter(self.bouquet_list.values())) elif not bouquet.startswith("1:7:1:0:0:0:0:0:0:0"): # Not a bouquet reference -> get bref from name bRef = self.bouquet_list.get(bouquet) if not bRef: _LOGGER.error('Specified bouquet "%s" could not be found', bouquet) return sources result = await self._call_api(PATH_EPGNOW, {"bRef": bRef}) if result and "events" in result and len(result["events"]) > 0: sources = { src["sname"]: src["sref"] for src in result["events"] if "sname" in src and "sref" in src } else: _LOGGER.warning("No sources could be loaded from specified bouquet.") return sources async def get_all_services(self) -> dict[str, Any]: """Get list of all services.""" return await self._call_api(PATH_GETALLSERVICES) async def get_bouquets(self, stype: SType = SType.TV) -> Bouquets: """Get list of bouquets.""" return await self._call_api(PATH_BOUQUETS, {"stype": str(stype)}) async def get_all_bouquets(self) -> Bouquets: """Get list of all tv and radio bouquets.""" return { "bouquets": [ *((await self.get_bouquets(SType.TV))["bouquets"]), *((await self.get_bouquets(SType.RADIO))["bouquets"]), ], } async def zap(self, source: str) -> bool: """Change channel to selected source. :param source: the sRef of the channel. """ return self._check_response_result( await self._call_api(PATH_ZAP, {"sRef": source}), ) async def _call_api( self, path: str, params: Mapping[str, str | int | bool] | None = None, ) -> dict[str, Any]: """Perform one api request operation.""" if self._session is None: self._session = aiohttp.ClientSession(self.base) async with self._session.get(path, params=params) as response: _LOGGER.debug("Got %d from: %s", response.status, response.request_info.url) if response.status == 401: raise InvalidAuthError if response.status != 200: _LOGGER.error( "Got %d from %s: %s", response.status, response.request_info.url, await response.text(), ) raise ConnectionError return dict(await response.json(content_type=None)) openwebifpy-4.3.1/openwebif/constants.py000066400000000000000000000010731473643061600204250ustar00rootroot00000000000000"""Constants for the use in OpenWebIf.""" DEFAULT_PORT = 80 # https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/OpenWebif-API-documentation/b5033cd37bb691643893c0d270f802a0b0e2b26b PATH_ABOUT = "/api/about" PATH_STATUSINFO = "/api/statusinfo" PATH_TUNERSIGNAL = "/api/tunersignal" PATH_VOL = "/api/vol" PATH_POWERSTATE = "/api/powerstate" PATH_REMOTECONTROL = "/api/remotecontrol" PATH_GETALLSERVICES = "/api/getallservices" PATH_BOUQUETS = "/api/bouquets" PATH_MESSAGE = "/api/message" PATH_ZAP = "/api/zap" PATH_EPGNOW = "/api/epgnow" PATH_GRAB = "/grab" openwebifpy-4.3.1/openwebif/enums.py000066400000000000000000000021621473643061600175400ustar00rootroot00000000000000"""Enums in regard to OpenWebIf.""" from enum import IntEnum, StrEnum class PlaybackType(IntEnum): """Enum for playback type.""" LIVE = 1 RECORDING = 2 NONE = 3 class MessageType(IntEnum): """Enum for message type.""" YESNO = 0 INFO = 1 WARNING = 2 ERROR = 3 class PowerState(IntEnum): """Enum for power state.""" TOGGLE_STANDBY = 0 DEEP_STANDBY = 1 REBOOT = 2 RESTART_ENIGMA = 3 WAKEUP = 4 STANDBY = 5 class RemoteControlCodes(IntEnum): """Enum for remote control codes.""" CHANNEL_UP = 402 CHANNEL_DOWN = 403 STOP = 128 PLAY = 207 PAUSE = 119 class ScreenGrabMode(StrEnum): """Enum for screen grab modes.""" ALL = "all" OSD = "osd" VIDEO = "video" PIP = "pip" LCD = "lcd" class ScreenGrabFormat(StrEnum): """Enum for screen grab formats.""" JPG = "jpg" PNG = "png" BMP = "bmp" class SetVolumeOption(StrEnum): """Enum for volume options.""" UP = "up" DOWN = "down" MUTE = "mute" class SType(StrEnum): """Enum for service type.""" TV = "tv" RADIO = "radio" openwebifpy-4.3.1/openwebif/error.py000066400000000000000000000001741473643061600175430ustar00rootroot00000000000000"""Module errors and exceptions.""" class InvalidAuthError(Exception): """Error to indicate there is invalid auth.""" openwebifpy-4.3.1/openwebif/py.typed000066400000000000000000000000001473643061600175230ustar00rootroot00000000000000openwebifpy-4.3.1/openwebif/types.py000066400000000000000000000002221473643061600175500ustar00rootroot00000000000000"""Types for openwebif.""" from typing import TypedDict class Bouquets(TypedDict): """The bouquets type.""" bouquets: list[list[str]] openwebifpy-4.3.1/pyproject.toml000066400000000000000000000063361473643061600170040ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name='openwebifpy' description="Provides a python interface to interact with a device running OpenWebIf" readme = "README.md" authors = [ {name = "Finbarr Brady", email = 'fbradyirl@github.io'}, {name = "Sidney Kuyateh", email = 'autinerd@kuyateh.eu'} ] maintainers = [ {name = "Sidney Kuyateh", email = 'autinerd@kuyateh.eu'} ] classifiers = [ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 3 :: Only', ] license = {text = "MIT License"} keywords=['openwebif'] dependencies = ["aiohttp", "yarl"] dynamic = ["version"] [project.urls] "Homepage" = "https://github.com/autinerd/openwebifpy" [tool.hatch.build.targets.wheel] packages = ["openwebif"] [tool.hatch.version] path = "openwebif/__init__.py" [tool.ruff.lint] select = ["ALL"] ignore = [ "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line "D406", # Section name should end with a newline "D407", # Section name underlining "E501", # line too long "E731", # do not assign a lambda expression, use a def "N806", "ERA001", "FBT", # Ignore ignored, as the rule is now back in preview/nursery, which cannot # be ignored anymore without warnings. # https://github.com/astral-sh/ruff/issues/7491 # "PLC1901", # Lots of false positives # False positives https://github.com/astral-sh/ruff/issues/5386 "PLC0208", # Use a sequence type instead of a `set` when iterating over values "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` "COM812", "ISC001" ] [tool.ruff.lint.extend-per-file-ignores] "tests/**" = ["S101"] [tool.mypy] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true warn_return_any = true warn_unreachable = true python_version = "3.12" plugins = "pydantic.mypy" show_error_codes = true local_partial_types = true strict_equality = true no_implicit_optional = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true enable_error_code = ["ignore-without-code", "redundant-self", "truthy-iterable"] disable_error_code = ["annotation-unchecked", "import-not-found", "import-untyped"] extra_checks = false no_implicit_reexport = true [tool.uv] dev-dependencies = [ "pytest>=8.3.3", ] openwebifpy-4.3.1/tests/000077500000000000000000000000001473643061600152225ustar00rootroot00000000000000openwebifpy-4.3.1/tests/__init__.py000066400000000000000000000000471473643061600173340ustar00rootroot00000000000000"""Unit test package for openwebif.""" openwebifpy-4.3.1/tests/test_openwebifpy.py000066400000000000000000000006431473643061600211650ustar00rootroot00000000000000"""Tests the api.""" import pytest import openwebif.api def test_create() -> None: """Test creating a new device.""" # Bogus config with pytest.raises(TypeError): openwebif.api.OpenWebIfDevice() # type: ignore[call-arg] def test_get_picon_name() -> None: """Tests whether the Picon name conversion works.""" assert openwebif.api.OpenWebIfDevice.get_picon_name("RTÉ One") == "rteone" openwebifpy-4.3.1/uv.lock000066400000000000000000000512571473643061600153760ustar00rootroot00000000000000version = 1 requires-python = ">=3.13" [[package]] name = "aiohappyeyeballs" version = "2.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/d9/e710a5c9e51b4d5a977c823ce323a81d344da8c1b6fba16bb270a8be800d/aiohappyeyeballs-2.4.2.tar.gz", hash = "sha256:4ca893e6c5c1f5bf3888b04cb5a3bee24995398efef6e0b9f747b5e89d84fd74", size = 18391 } wheels = [ { url = "https://files.pythonhosted.org/packages/13/64/40165ff77ade5203284e3015cf88e11acb07d451f6bf83fff71192912a0d/aiohappyeyeballs-2.4.2-py3-none-any.whl", hash = "sha256:8522691d9a154ba1145b157d6d5c15e5c692527ce6a53c5e5f9876977f6dab2f", size = 14105 }, ] [[package]] name = "aiohttp" version = "3.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/97/15c51bbfcc184bcb4d473b7b02e7b54b6978e0083556a9cd491875cf11f7/aiohttp-3.10.6.tar.gz", hash = "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336", size = 7538429 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/7f/89eb922fda25d5b9c7c08d14d50c788d998f148210478059b7549040424a/aiohttp-3.10.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd", size = 575722 }, { url = "https://files.pythonhosted.org/packages/84/6d/eb3965c55748f960751b752969983982a995d2aa21f023ed30fe5a471629/aiohttp-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1", size = 391518 }, { url = "https://files.pythonhosted.org/packages/78/4a/98c9d9cee601477eda8f851376eff88e864e9f3147cbc3a428da47d90ed0/aiohttp-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d", size = 387037 }, { url = "https://files.pythonhosted.org/packages/2d/b0/6136aefae0f0d2abe4a435af71a944781e37bbe6fd836a23ff41bbba0682/aiohttp-3.10.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d", size = 1286703 }, { url = "https://files.pythonhosted.org/packages/cd/8a/a17ec94a7b6394efeeaca16df8d1e9359f0aa83548e40bf16b5853ed7684/aiohttp-3.10.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f", size = 1323244 }, { url = "https://files.pythonhosted.org/packages/35/37/4cf6d2a8dce91ea7ff8b8ed8e1ef5c6a5934e07b4da5993ae95660b7cfbc/aiohttp-3.10.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d", size = 1368034 }, { url = "https://files.pythonhosted.org/packages/0d/d5/e939fcf26bd5c7760a1b71eff7396f6ca0e3c807088086551db28af0c090/aiohttp-3.10.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf", size = 1282395 }, { url = "https://files.pythonhosted.org/packages/46/44/85d5d61b3ac50f30766cd2c1d22e6f937f027922621fc91581ead05749f6/aiohttp-3.10.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb", size = 1236147 }, { url = "https://files.pythonhosted.org/packages/61/35/43eee26590f369906151cea78297554304ed2ceda5a5ed69cc2e907e9903/aiohttp-3.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8", size = 1249963 }, { url = "https://files.pythonhosted.org/packages/44/b5/e099ad2bf7ad6ab5bb685f66a7599dc7f9fb4879eb987a4bf02ca2886974/aiohttp-3.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d", size = 1248579 }, { url = "https://files.pythonhosted.org/packages/85/81/520348e8ec472679e65deb87c2a2bb2ad2c40e328746245bd35251b7ee4f/aiohttp-3.10.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f", size = 1293005 }, { url = "https://files.pythonhosted.org/packages/e5/a8/1ddd2af786c3b4f30187bc98464b8e3c54c6bbf18062a20291c6b5b03f27/aiohttp-3.10.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413", size = 1319740 }, { url = "https://files.pythonhosted.org/packages/0a/6f/a757fdf01ce4d20fcfee35af3b63a2393dbd3478873c4ea9aaad24b093f1/aiohttp-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362", size = 1281177 }, { url = "https://files.pythonhosted.org/packages/9d/d9/e5866f341cfad4de82caf570218a424f96914192a9230dd6f6dfe4653a93/aiohttp-3.10.6-cp313-cp313-win32.whl", hash = "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5", size = 357148 }, { url = "https://files.pythonhosted.org/packages/57/cc/ba781a170fd4405819cc988026cfa16a9397ffebf5639dc84ad65d518448/aiohttp-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206", size = 376413 }, ] [[package]] name = "aiosignal" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, ] [[package]] name = "attrs" version = "24.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "frozenlist" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cf/3d/2102257e7acad73efc4a0c306ad3953f68c504c16982bbdfee3ad75d8085/frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", size = 37820 } wheels = [ { url = "https://files.pythonhosted.org/packages/83/10/466fe96dae1bff622021ee687f68e5524d6392b0a2f80d05001cd3a451ba/frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", size = 11552 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "multidict" version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } wheels = [ { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] [[package]] name = "openwebifpy" version = "4.2.7" source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "yarl" }, ] [package.dev-dependencies] dev = [ { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "aiohttp" }, { name = "yarl" }, ] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8.3.3" }] [[package]] name = "packaging" version = "24.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } wheels = [ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pytest" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] [[package]] name = "yarl" version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/6e/b26e831b6abede32fba3763131eb2c52987f574277daa65e10a5fda6021c/yarl-1.13.0.tar.gz", hash = "sha256:02f117a63d11c8c2ada229029f8bb444a811e62e5041da962de548f26ac2c40f", size = 165688 } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/9d/62e0325479f6e225c006db065b81d92a214e15dbd9d5f08b7f58cbea2a1d/yarl-1.13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:91251614cca1ba4ab0507f1ba5f5a44e17a5e9a4c7f0308ea441a994bdac3fc7", size = 186212 }, { url = "https://files.pythonhosted.org/packages/33/e6/216ca46bb456cc6942f0098abb67b192c52733292d37cb4f230889c8c826/yarl-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe6946c3cbcfbed67c5e50dae49baff82ad054aaa10ff7a4db8dfac646b7b479", size = 114218 }, { url = "https://files.pythonhosted.org/packages/c4/9d/1e937ba8820129effa4fcb8d7188e990711d73f6eaff0888a9205e33cecd/yarl-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de97ee57e00a82ebb8c378fc73c5d9a773e4c2cec8079ff34ebfef61c8ba5b11", size = 112118 }, { url = "https://files.pythonhosted.org/packages/62/19/9f60d2c8bfd9820708268c4466e4d52d64b6ecec26557a26d9a7c3d60991/yarl-1.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1129737da2291c9952a93c015e73583dd66054f3ae991c8674f6e39c46d95dd3", size = 471387 }, { url = "https://files.pythonhosted.org/packages/4c/a9/d6936a780b35a202a9eb93905d283da4243fcfca85f464571d7ce6f5759e/yarl-1.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37049eb26d637a5b2f00562f65aad679f5d231c4c044edcd88320542ad66a2d9", size = 485837 }, { url = "https://files.pythonhosted.org/packages/f1/62/1903cb89c2b069c985fb0577a152652b80a8700b6f96a72c2b127c00cac3/yarl-1.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d15aff3477fecb7a469d1fdf5939a686fbc5a16858022897d3e9fc99301f19", size = 486662 }, { url = "https://files.pythonhosted.org/packages/ea/70/17a1092eec93b9b2ca2fe7e9c854b52f968a5457a16c0192cb1684f666e9/yarl-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa187a8599e0425f26b25987d884a8b67deb5565f1c450c3a6e8d3de2cdc8715", size = 478867 }, { url = "https://files.pythonhosted.org/packages/3e/ab/20d8b6ff384b126e2aca1546b8ba93e4a4aee35cfa68043b8015cf2fb309/yarl-1.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d95fcc9508390db73a0f1c7e78d9a1b1a3532a3f34ceff97c0b3b04140fbe6e4", size = 456455 }, { url = "https://files.pythonhosted.org/packages/6a/31/66bebe242af5f0615b2a6f7ae9ac37633983c621eb333367830500f8f954/yarl-1.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d04ea92a3643a9bb28aa6954fff718342caab2cc3d25d0160fe16e26c4a9acb7", size = 474964 }, { url = "https://files.pythonhosted.org/packages/69/02/67d94189a94d191edf47b8a34721e0e7265556e821e9bb2f856da7f8af39/yarl-1.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2842a89b697d8ca3dda6a25b4e4d835d14afe25a315c8a79dbdf5f70edfd0960", size = 477474 }, { url = "https://files.pythonhosted.org/packages/60/33/a746b05fedc340e8055d38b3f892418577252b1dbd6be474faebe1ceb9f3/yarl-1.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db463fce425f935eee04a9182c74fdf9ed90d3bd2079d4a17f8fb7a2d7c11009", size = 491950 }, { url = "https://files.pythonhosted.org/packages/5b/75/9759d92dc66108264f305f1ddb3ae02bcc247849a6673ebb678a082d398e/yarl-1.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3ff602aa84420b301c083ae7f07df858ae8e371bf3be294397bda3e0b27c6290", size = 502141 }, { url = "https://files.pythonhosted.org/packages/e8/8d/4d9f6fa810eca7e07ae7bc6eea0136a4268a32439e6ce6e7454470c51dac/yarl-1.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a9a1a600e8449f3a24bc7dca513be8d69db173fe842e8332a7318b5b8757a6af", size = 492846 }, { url = "https://files.pythonhosted.org/packages/77/b1/da12907ccb4cea4781357ec027e81e141251726aeffa6ea2c8d1f62cc117/yarl-1.13.0-cp313-cp313-win32.whl", hash = "sha256:5540b4896b244a6539f22b613b32b5d1b737e08011aa4ed56644cb0519d687df", size = 486417 }, { url = "https://files.pythonhosted.org/packages/66/9e/05d133e7523035517e0dc912a59779dcfd5e978aff32c1c2a3cbc1fd4e7c/yarl-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:08a3b0b8d10092dade46424fe775f2c9bc32e5a985fdd6afe410fe28598db6b2", size = 493756 }, { url = "https://files.pythonhosted.org/packages/10/ae/c3c059042053b92ae25363818901d0634708a3a85048e5ac835bd547107e/yarl-1.13.0-py3-none-any.whl", hash = "sha256:c7d35ff2a5a51bc6d40112cdb4ca3fd9636482ce8c6ceeeee2301e34f7ed7556", size = 39813 }, ]